From f3427295a99f7aa4a45412f699ec6b3cc41b16c2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:13:58 +0000 Subject: [PATCH 01/53] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8d0e94f..5a0c69c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/finch-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 686b27e6d19cc5858b420e2b1bb265f50791c7ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:30:26 +0000 Subject: [PATCH 02/53] feat(api): add per endpoint security --- .stats.yml | 2 +- src/finch/resources/access_tokens.py | 4 + src/finch/resources/account.py | 4 + src/finch/resources/connect/sessions.py | 4 + src/finch/resources/hris/benefits/benefits.py | 10 + .../resources/hris/benefits/individuals.py | 8 + src/finch/resources/hris/company/company.py | 2 + .../pay_statement_item/pay_statement_item.py | 2 + .../hris/company/pay_statement_item/rules.py | 8 + src/finch/resources/hris/directory.py | 2 + src/finch/resources/hris/documents.py | 4 + src/finch/resources/hris/employments.py | 2 + src/finch/resources/hris/individuals.py | 2 + src/finch/resources/hris/pay_statements.py | 2 + src/finch/resources/hris/payments.py | 2 + src/finch/resources/jobs/automated.py | 6 + src/finch/resources/jobs/manual.py | 2 + src/finch/resources/payroll/pay_groups.py | 4 + src/finch/resources/providers.py | 2 + src/finch/resources/request_forwarding.py | 2 + src/finch/resources/sandbox/company.py | 2 + .../resources/sandbox/connections/accounts.py | 4 + .../sandbox/connections/connections.py | 2 + src/finch/resources/sandbox/directory.py | 2 + src/finch/resources/sandbox/employment.py | 2 + src/finch/resources/sandbox/individual.py | 2 + .../resources/sandbox/jobs/configuration.py | 4 + src/finch/resources/sandbox/jobs/jobs.py | 2 + src/finch/resources/sandbox/payment.py | 2 + tests/conftest.py | 17 +- tests/test_client.py | 274 ++++++++++++++++-- 31 files changed, 358 insertions(+), 29 deletions(-) diff --git a/.stats.yml b/.stats.yml index b15bfab0..4db1b6f9 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-46f433f34d440aa1dfcc48cc8d822c598571b68be2f723ec99e1b4fba6c13b1e.yml openapi_spec_hash: 5b5cd728776723ac773900f7e8a32c05 -config_hash: 0892e2e0eeb0343a022afa62e9080dd1 +config_hash: 83522e0e335cf983f8d2119c1f2bba18 diff --git a/src/finch/resources/access_tokens.py b/src/finch/resources/access_tokens.py index 39599bc9..f1f46034 100644 --- a/src/finch/resources/access_tokens.py +++ b/src/finch/resources/access_tokens.py @@ -71,6 +71,8 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Authorization": omit, **(extra_headers or {})} + if not is_given(client_id): if self._client.client_id is None: raise ValueError( @@ -157,6 +159,8 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Authorization": omit, **(extra_headers or {})} + if not is_given(client_id): if self._client.client_id is None: raise ValueError( diff --git a/src/finch/resources/account.py b/src/finch/resources/account.py index 394a29e8..ac8583a2 100644 --- a/src/finch/resources/account.py +++ b/src/finch/resources/account.py @@ -47,6 +47,7 @@ def disconnect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DisconnectResponse: """Disconnect one or more `access_token`s from your application.""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/disconnect", options=make_request_options( @@ -66,6 +67,7 @@ def introspect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Introspection: """Read account information associated with an `access_token`""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/introspect", options=make_request_options( @@ -106,6 +108,7 @@ async def disconnect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DisconnectResponse: """Disconnect one or more `access_token`s from your application.""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/disconnect", options=make_request_options( @@ -125,6 +128,7 @@ async def introspect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Introspection: """Read account information associated with an `access_token`""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/introspect", options=make_request_options( diff --git a/src/finch/resources/connect/sessions.py b/src/finch/resources/connect/sessions.py index f402a565..648deb68 100644 --- a/src/finch/resources/connect/sessions.py +++ b/src/finch/resources/connect/sessions.py @@ -104,6 +104,7 @@ def new( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/connect/sessions", body=maybe_transform( @@ -177,6 +178,7 @@ def reauthenticate( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/connect/sessions/reauthenticate", body=maybe_transform( @@ -278,6 +280,7 @@ async def new( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/connect/sessions", body=await async_maybe_transform( @@ -351,6 +354,7 @@ async def reauthenticate( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/connect/sessions/reauthenticate", body=await async_maybe_transform( diff --git a/src/finch/resources/hris/benefits/benefits.py b/src/finch/resources/hris/benefits/benefits.py index 9d5d0dde..91236b33 100644 --- a/src/finch/resources/hris/benefits/benefits.py +++ b/src/finch/resources/hris/benefits/benefits.py @@ -106,6 +106,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/employer/benefits", body=maybe_transform( @@ -155,6 +156,7 @@ def retrieve( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/employer/benefits/{benefit_id}", options=make_request_options( @@ -198,6 +200,7 @@ def update( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( f"/employer/benefits/{benefit_id}", body=maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams), @@ -236,6 +239,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits", page=SyncSinglePage[CompanyBenefit], @@ -274,6 +278,7 @@ def list_supported_benefits( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits/meta", page=SyncSinglePage[SupportedBenefit], @@ -356,6 +361,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/employer/benefits", body=await async_maybe_transform( @@ -407,6 +413,7 @@ async def retrieve( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/employer/benefits/{benefit_id}", options=make_request_options( @@ -452,6 +459,7 @@ async def update( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( f"/employer/benefits/{benefit_id}", body=await async_maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams), @@ -492,6 +500,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits", page=AsyncSinglePage[CompanyBenefit], @@ -530,6 +539,7 @@ def list_supported_benefits( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits/meta", page=AsyncSinglePage[SupportedBenefit], diff --git a/src/finch/resources/hris/benefits/individuals.py b/src/finch/resources/hris/benefits/individuals.py index cc8d67d3..c03f86fb 100644 --- a/src/finch/resources/hris/benefits/individuals.py +++ b/src/finch/resources/hris/benefits/individuals.py @@ -83,6 +83,7 @@ def enroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( f"/employer/benefits/{benefit_id}/individuals", body=maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), @@ -126,6 +127,7 @@ def enrolled_ids( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/employer/benefits/{benefit_id}/enrolled", options=make_request_options( @@ -172,6 +174,7 @@ def retrieve_many_benefits( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( f"/employer/benefits/{benefit_id}/individuals", page=SyncSinglePage[IndividualBenefit], @@ -222,6 +225,7 @@ def unenroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._delete( f"/employer/benefits/{benefit_id}/individuals", body=maybe_transform( @@ -295,6 +299,7 @@ async def enroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( f"/employer/benefits/{benefit_id}/individuals", body=await async_maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), @@ -338,6 +343,7 @@ async def enrolled_ids( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/employer/benefits/{benefit_id}/enrolled", options=make_request_options( @@ -384,6 +390,7 @@ def retrieve_many_benefits( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( f"/employer/benefits/{benefit_id}/individuals", page=AsyncSinglePage[IndividualBenefit], @@ -434,6 +441,7 @@ async def unenroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._delete( f"/employer/benefits/{benefit_id}/individuals", body=await async_maybe_transform( diff --git a/src/finch/resources/hris/company/company.py b/src/finch/resources/hris/company/company.py index 159b6e64..1bc042b7 100644 --- a/src/finch/resources/hris/company/company.py +++ b/src/finch/resources/hris/company/company.py @@ -74,6 +74,7 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/employer/company", options=make_request_options( @@ -136,6 +137,7 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/employer/company", options=make_request_options( diff --git a/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py b/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py index d2d83911..5254872d 100644 --- a/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py +++ b/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py @@ -98,6 +98,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item", page=SyncResponsesPage[PayStatementItemListResponse], @@ -190,6 +191,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item", page=AsyncResponsesPage[PayStatementItemListResponse], diff --git a/src/finch/resources/hris/company/pay_statement_item/rules.py b/src/finch/resources/hris/company/pay_statement_item/rules.py index 64071a73..7fc7d9ad 100644 --- a/src/finch/resources/hris/company/pay_statement_item/rules.py +++ b/src/finch/resources/hris/company/pay_statement_item/rules.py @@ -90,6 +90,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/employer/pay-statement-item/rule", body=maybe_transform( @@ -141,6 +142,7 @@ def update( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( f"/employer/pay-statement-item/rule/{rule_id}", body=maybe_transform({"optional_property": optional_property}, rule_update_params.RuleUpdateParams), @@ -179,6 +181,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item/rule", page=SyncResponsesPage[RuleListResponse], @@ -220,6 +223,7 @@ def delete( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._delete( f"/employer/pay-statement-item/rule/{rule_id}", options=make_request_options( @@ -294,6 +298,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/employer/pay-statement-item/rule", body=await async_maybe_transform( @@ -345,6 +350,7 @@ async def update( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( f"/employer/pay-statement-item/rule/{rule_id}", body=await async_maybe_transform( @@ -385,6 +391,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item/rule", page=AsyncResponsesPage[RuleListResponse], @@ -426,6 +433,7 @@ async def delete( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._delete( f"/employer/pay-statement-item/rule/{rule_id}", options=make_request_options( diff --git a/src/finch/resources/hris/directory.py b/src/finch/resources/hris/directory.py index 32068b06..50cbae78 100644 --- a/src/finch/resources/hris/directory.py +++ b/src/finch/resources/hris/directory.py @@ -71,6 +71,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/directory", page=SyncIndividualsPage[IndividualInDirectory], @@ -185,6 +186,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/directory", page=AsyncIndividualsPage[IndividualInDirectory], diff --git a/src/finch/resources/hris/documents.py b/src/finch/resources/hris/documents.py index e7dc7c73..5c56765a 100644 --- a/src/finch/resources/hris/documents.py +++ b/src/finch/resources/hris/documents.py @@ -82,6 +82,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/employer/documents", options=make_request_options( @@ -133,6 +134,7 @@ def retreive( """ if not document_id: raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return cast( DocumentRetreiveResponse, self._get( @@ -212,6 +214,7 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/employer/documents", options=make_request_options( @@ -263,6 +266,7 @@ async def retreive( """ if not document_id: raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return cast( DocumentRetreiveResponse, await self._get( diff --git a/src/finch/resources/hris/employments.py b/src/finch/resources/hris/employments.py index 6374b0b7..07d3087b 100644 --- a/src/finch/resources/hris/employments.py +++ b/src/finch/resources/hris/employments.py @@ -68,6 +68,7 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/employment", page=SyncResponsesPage[EmploymentDataResponse], @@ -134,6 +135,7 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/employment", page=AsyncResponsesPage[EmploymentDataResponse], diff --git a/src/finch/resources/hris/individuals.py b/src/finch/resources/hris/individuals.py index b4a966c0..64f04179 100644 --- a/src/finch/resources/hris/individuals.py +++ b/src/finch/resources/hris/individuals.py @@ -67,6 +67,7 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/individual", page=SyncResponsesPage[IndividualResponse], @@ -138,6 +139,7 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/individual", page=AsyncResponsesPage[IndividualResponse], diff --git a/src/finch/resources/hris/pay_statements.py b/src/finch/resources/hris/pay_statements.py index bcae95c8..64312286 100644 --- a/src/finch/resources/hris/pay_statements.py +++ b/src/finch/resources/hris/pay_statements.py @@ -71,6 +71,7 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement", page=SyncResponsesPage[PayStatementResponse], @@ -142,6 +143,7 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement", page=AsyncResponsesPage[PayStatementResponse], diff --git a/src/finch/resources/hris/payments.py b/src/finch/resources/hris/payments.py index a795090c..2b226949 100644 --- a/src/finch/resources/hris/payments.py +++ b/src/finch/resources/hris/payments.py @@ -74,6 +74,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/payment", page=SyncSinglePage[Payment], @@ -148,6 +149,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/payment", page=AsyncSinglePage[Payment], diff --git a/src/finch/resources/jobs/automated.py b/src/finch/resources/jobs/automated.py index 687e1389..fa266ac7 100644 --- a/src/finch/resources/jobs/automated.py +++ b/src/finch/resources/jobs/automated.py @@ -137,6 +137,7 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AutomatedCreateResponse: + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/jobs/automated", body=maybe_transform( @@ -177,6 +178,7 @@ def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/jobs/automated/{job_id}", options=make_request_options( @@ -216,6 +218,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/jobs/automated", options=make_request_options( @@ -351,6 +354,7 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AutomatedCreateResponse: + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/jobs/automated", body=await async_maybe_transform( @@ -391,6 +395,7 @@ async def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/jobs/automated/{job_id}", options=make_request_options( @@ -430,6 +435,7 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/jobs/automated", options=make_request_options( diff --git a/src/finch/resources/jobs/manual.py b/src/finch/resources/jobs/manual.py index 9e99c9d7..d391d137 100644 --- a/src/finch/resources/jobs/manual.py +++ b/src/finch/resources/jobs/manual.py @@ -62,6 +62,7 @@ def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/jobs/manual/{job_id}", options=make_request_options( @@ -118,6 +119,7 @@ async def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/jobs/manual/{job_id}", options=make_request_options( diff --git a/src/finch/resources/payroll/pay_groups.py b/src/finch/resources/payroll/pay_groups.py index 0202884e..91c5edbf 100644 --- a/src/finch/resources/payroll/pay_groups.py +++ b/src/finch/resources/payroll/pay_groups.py @@ -67,6 +67,7 @@ def retrieve( """ if not pay_group_id: raise ValueError(f"Expected a non-empty value for `pay_group_id` but received {pay_group_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/employer/pay-groups/{pay_group_id}", options=make_request_options( @@ -106,6 +107,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-groups", page=SyncSinglePage[PayGroupListResponse], @@ -175,6 +177,7 @@ async def retrieve( """ if not pay_group_id: raise ValueError(f"Expected a non-empty value for `pay_group_id` but received {pay_group_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/employer/pay-groups/{pay_group_id}", options=make_request_options( @@ -216,6 +219,7 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-groups", page=AsyncSinglePage[PayGroupListResponse], diff --git a/src/finch/resources/providers.py b/src/finch/resources/providers.py index 9fc6fad3..2e37ff56 100644 --- a/src/finch/resources/providers.py +++ b/src/finch/resources/providers.py @@ -47,6 +47,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncSinglePage[ProviderListResponse]: """Return details on all available payroll and HR systems.""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/providers", page=SyncSinglePage[ProviderListResponse], @@ -88,6 +89,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ProviderListResponse, AsyncSinglePage[ProviderListResponse]]: """Return details on all available payroll and HR systems.""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/providers", page=AsyncSinglePage[ProviderListResponse], diff --git a/src/finch/resources/request_forwarding.py b/src/finch/resources/request_forwarding.py index dacb93fd..01bea5a8 100644 --- a/src/finch/resources/request_forwarding.py +++ b/src/finch/resources/request_forwarding.py @@ -87,6 +87,7 @@ def forward( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/forward", body=maybe_transform( @@ -174,6 +175,7 @@ async def forward( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/forward", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/company.py b/src/finch/resources/sandbox/company.py index 45c87dc9..714bc7b1 100644 --- a/src/finch/resources/sandbox/company.py +++ b/src/finch/resources/sandbox/company.py @@ -85,6 +85,7 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( "/sandbox/company", body=maybe_transform( @@ -172,6 +173,7 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( "/sandbox/company", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/connections/accounts.py b/src/finch/resources/sandbox/connections/accounts.py index e38e2a2d..e8d168b9 100644 --- a/src/finch/resources/sandbox/connections/accounts.py +++ b/src/finch/resources/sandbox/connections/accounts.py @@ -72,6 +72,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/sandbox/connections/accounts", body=maybe_transform( @@ -114,6 +115,7 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( "/sandbox/connections/accounts", body=maybe_transform({"connection_status": connection_status}, account_update_params.AccountUpdateParams), @@ -175,6 +177,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/sandbox/connections/accounts", body=await async_maybe_transform( @@ -217,6 +220,7 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( "/sandbox/connections/accounts", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/connections/connections.py b/src/finch/resources/sandbox/connections/connections.py index c4c35dc3..a3f0c4b1 100644 --- a/src/finch/resources/sandbox/connections/connections.py +++ b/src/finch/resources/sandbox/connections/connections.py @@ -83,6 +83,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/sandbox/connections", body=maybe_transform( @@ -157,6 +158,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/sandbox/connections", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/directory.py b/src/finch/resources/sandbox/directory.py index 2afba6a5..6096fdc0 100644 --- a/src/finch/resources/sandbox/directory.py +++ b/src/finch/resources/sandbox/directory.py @@ -65,6 +65,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/sandbox/directory", body=maybe_transform(body, Iterable[directory_create_params.Body]), @@ -121,6 +122,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/sandbox/directory", body=await async_maybe_transform(body, Iterable[directory_create_params.Body]), diff --git a/src/finch/resources/sandbox/employment.py b/src/finch/resources/sandbox/employment.py index ede9a473..8591ced6 100644 --- a/src/finch/resources/sandbox/employment.py +++ b/src/finch/resources/sandbox/employment.py @@ -122,6 +122,7 @@ def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( f"/sandbox/employment/{individual_id}", body=maybe_transform( @@ -254,6 +255,7 @@ async def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( f"/sandbox/employment/{individual_id}", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/individual.py b/src/finch/resources/sandbox/individual.py index 5b9041c0..399a88c2 100644 --- a/src/finch/resources/sandbox/individual.py +++ b/src/finch/resources/sandbox/individual.py @@ -113,6 +113,7 @@ def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( f"/sandbox/individual/{individual_id}", body=maybe_transform( @@ -231,6 +232,7 @@ async def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( f"/sandbox/individual/{individual_id}", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/jobs/configuration.py b/src/finch/resources/sandbox/jobs/configuration.py index f8839411..e3239cc6 100644 --- a/src/finch/resources/sandbox/jobs/configuration.py +++ b/src/finch/resources/sandbox/jobs/configuration.py @@ -51,6 +51,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ConfigurationRetrieveResponse: """Get configurations for sandbox jobs""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/sandbox/jobs/configuration", options=make_request_options( @@ -83,6 +84,7 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( "/sandbox/jobs/configuration", body=maybe_transform( @@ -130,6 +132,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ConfigurationRetrieveResponse: """Get configurations for sandbox jobs""" + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/sandbox/jobs/configuration", options=make_request_options( @@ -162,6 +165,7 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( "/sandbox/jobs/configuration", body=await async_maybe_transform( diff --git a/src/finch/resources/sandbox/jobs/jobs.py b/src/finch/resources/sandbox/jobs/jobs.py index 070bd293..4ac88a37 100644 --- a/src/finch/resources/sandbox/jobs/jobs.py +++ b/src/finch/resources/sandbox/jobs/jobs.py @@ -77,6 +77,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/sandbox/jobs", body=maybe_transform({"type": type}, job_create_params.JobCreateParams), @@ -137,6 +138,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/sandbox/jobs", body=await async_maybe_transform({"type": type}, job_create_params.JobCreateParams), diff --git a/src/finch/resources/sandbox/payment.py b/src/finch/resources/sandbox/payment.py index 2506f49d..f08880a7 100644 --- a/src/finch/resources/sandbox/payment.py +++ b/src/finch/resources/sandbox/payment.py @@ -67,6 +67,7 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/sandbox/payment", body=maybe_transform( @@ -131,6 +132,7 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/sandbox/payment", body=await async_maybe_transform( diff --git a/tests/conftest.py b/tests/conftest.py index 6631269d..3700f724 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,6 +46,8 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" +client_id = "4ab15e51-11ad-49f4-acae-f343b7794375" +client_secret = "My Client Secret" @pytest.fixture(scope="session") @@ -54,7 +56,13 @@ def client(request: FixtureRequest) -> Iterator[Finch]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Finch(base_url=base_url, access_token=access_token, _strict_response_validation=strict) as client: + with Finch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=strict, + ) as client: yield client @@ -79,6 +87,11 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncFinch]: raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") async with AsyncFinch( - base_url=base_url, access_token=access_token, _strict_response_validation=strict, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=strict, + http_client=http_client, ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index c6b61701..67ce24a4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -40,6 +40,8 @@ T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" +client_id = "4ab15e51-11ad-49f4-acae-f343b7794375" +client_secret = "My Client Secret" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -132,6 +134,14 @@ def test_copy(self, client: Finch) -> None: assert copied.access_token == "another My Access Token" assert client.access_token == "My Access Token" + copied = client.copy(client_id="another 4ab15e51-11ad-49f4-acae-f343b7794375") + assert copied.client_id == "another 4ab15e51-11ad-49f4-acae-f343b7794375" + assert client.client_id == "4ab15e51-11ad-49f4-acae-f343b7794375" + + copied = client.copy(client_secret="another My Client Secret") + assert copied.client_secret == "another My Client Secret" + assert client.client_secret == "My Client Secret" + def test_copy_default_options(self, client: Finch) -> None: # options that have a default are overridden correctly copied = client.copy(max_retries=7) @@ -152,6 +162,8 @@ def test_copy_default_headers(self) -> None: client = Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -188,7 +200,12 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = Finch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, default_query={"foo": "bar"} + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + default_query={"foo": "bar"}, ) assert _get_params(client)["foo"] == "bar" @@ -314,7 +331,12 @@ def test_request_timeout(self, client: Finch) -> None: def test_client_timeout_option(self) -> None: client = Finch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, timeout=httpx.Timeout(0) + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + timeout=httpx.Timeout(0), ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -327,7 +349,12 @@ def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: client = Finch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -339,7 +366,12 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Finch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -351,7 +383,12 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Finch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -366,6 +403,8 @@ async def test_invalid_http_client(self) -> None: Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=cast(Any, http_client), ) @@ -374,6 +413,8 @@ def test_default_headers_option(self) -> None: test_client = Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -384,6 +425,8 @@ def test_default_headers_option(self) -> None: test_client2 = Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -398,11 +441,29 @@ def test_default_headers_option(self) -> None: test_client2.close() def test_validate_headers(self) -> None: - client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + client = Finch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {access_token}" - client2 = Finch(base_url=base_url, access_token=None, _strict_response_validation=True) + with update_env( + **{ + "FINCH_CLIENT_ID": Omit(), + "FINCH_CLIENT_SECRET": Omit(), + } + ): + client2 = Finch( + base_url=base_url, + access_token=None, + client_id=None, + client_secret=None, + _strict_response_validation=True, + ) with pytest.raises( TypeError, @@ -419,6 +480,8 @@ def test_default_query_option(self) -> None: client = Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_query={"query_param": "bar"}, ) @@ -593,6 +656,8 @@ def mock_handler(request: httpx.Request) -> httpx.Response: with Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), ) as client: @@ -687,7 +752,11 @@ class Model(BaseModel): def test_base_url_setter(self) -> None: client = Finch( - base_url="https://example.com/from_init", access_token=access_token, _strict_response_validation=True + base_url="https://example.com/from_init", + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, ) assert client.base_url == "https://example.com/from_init/" @@ -699,7 +768,12 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(FINCH_BASE_URL="http://localhost:5000/from/env"): - client = Finch(access_token=access_token, _strict_response_validation=True) + client = Finch( + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( @@ -708,11 +782,15 @@ def test_base_url_env(self) -> None: Finch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, ), Finch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -736,11 +814,15 @@ def test_base_url_trailing_slash(self, client: Finch) -> None: Finch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, ), Finch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -764,11 +846,15 @@ def test_base_url_no_trailing_slash(self, client: Finch) -> None: Finch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, ), Finch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -787,7 +873,13 @@ def test_absolute_request_url(self, client: Finch) -> None: client.close() def test_copied_client_does_not_close_http(self) -> None: - test_client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = Finch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) assert not test_client.is_closed() copied = test_client.copy() @@ -798,7 +890,13 @@ def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() def test_client_context_manager(self) -> None: - test_client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = Finch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -822,6 +920,8 @@ def test_client_max_retries_validation(self) -> None: Finch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, max_retries=cast(Any, None), ) @@ -833,12 +933,24 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + strict_client = Finch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - non_strict_client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=False) + non_strict_client = Finch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=False, + ) response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1051,6 +1163,14 @@ def test_copy(self, async_client: AsyncFinch) -> None: assert copied.access_token == "another My Access Token" assert async_client.access_token == "My Access Token" + copied = async_client.copy(client_id="another 4ab15e51-11ad-49f4-acae-f343b7794375") + assert copied.client_id == "another 4ab15e51-11ad-49f4-acae-f343b7794375" + assert async_client.client_id == "4ab15e51-11ad-49f4-acae-f343b7794375" + + copied = async_client.copy(client_secret="another My Client Secret") + assert copied.client_secret == "another My Client Secret" + assert async_client.client_secret == "My Client Secret" + def test_copy_default_options(self, async_client: AsyncFinch) -> None: # options that have a default are overridden correctly copied = async_client.copy(max_retries=7) @@ -1071,6 +1191,8 @@ async def test_copy_default_headers(self) -> None: client = AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -1107,7 +1229,12 @@ async def test_copy_default_headers(self) -> None: async def test_copy_default_query(self) -> None: client = AsyncFinch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, default_query={"foo": "bar"} + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + default_query={"foo": "bar"}, ) assert _get_params(client)["foo"] == "bar" @@ -1235,7 +1362,12 @@ async def test_request_timeout(self, async_client: AsyncFinch) -> None: async def test_client_timeout_option(self) -> None: client = AsyncFinch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, timeout=httpx.Timeout(0) + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + timeout=httpx.Timeout(0), ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1248,7 +1380,12 @@ async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: client = AsyncFinch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1260,7 +1397,12 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncFinch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1272,7 +1414,12 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncFinch( - base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + http_client=http_client, ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1287,6 +1434,8 @@ def test_invalid_http_client(self) -> None: AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=cast(Any, http_client), ) @@ -1295,6 +1444,8 @@ async def test_default_headers_option(self) -> None: test_client = AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -1305,6 +1456,8 @@ async def test_default_headers_option(self) -> None: test_client2 = AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1319,11 +1472,29 @@ async def test_default_headers_option(self) -> None: await test_client2.close() def test_validate_headers(self) -> None: - client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + client = AsyncFinch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {access_token}" - client2 = AsyncFinch(base_url=base_url, access_token=None, _strict_response_validation=True) + with update_env( + **{ + "FINCH_CLIENT_ID": Omit(), + "FINCH_CLIENT_SECRET": Omit(), + } + ): + client2 = AsyncFinch( + base_url=base_url, + access_token=None, + client_id=None, + client_secret=None, + _strict_response_validation=True, + ) with pytest.raises( TypeError, @@ -1340,6 +1511,8 @@ async def test_default_query_option(self) -> None: client = AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, default_query={"query_param": "bar"}, ) @@ -1514,6 +1687,8 @@ async def mock_handler(request: httpx.Request) -> httpx.Response: async with AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), ) as client: @@ -1612,7 +1787,11 @@ class Model(BaseModel): async def test_base_url_setter(self) -> None: client = AsyncFinch( - base_url="https://example.com/from_init", access_token=access_token, _strict_response_validation=True + base_url="https://example.com/from_init", + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, ) assert client.base_url == "https://example.com/from_init/" @@ -1624,7 +1803,12 @@ async def test_base_url_setter(self) -> None: async def test_base_url_env(self) -> None: with update_env(FINCH_BASE_URL="http://localhost:5000/from/env"): - client = AsyncFinch(access_token=access_token, _strict_response_validation=True) + client = AsyncFinch( + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( @@ -1633,11 +1817,15 @@ async def test_base_url_env(self) -> None: AsyncFinch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, ), AsyncFinch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1661,11 +1849,15 @@ async def test_base_url_trailing_slash(self, client: AsyncFinch) -> None: AsyncFinch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, ), AsyncFinch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1689,11 +1881,15 @@ async def test_base_url_no_trailing_slash(self, client: AsyncFinch) -> None: AsyncFinch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, ), AsyncFinch( base_url="http://localhost:5000/custom/path/", access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1712,7 +1908,13 @@ async def test_absolute_request_url(self, client: AsyncFinch) -> None: await client.close() async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = AsyncFinch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) assert not test_client.is_closed() copied = test_client.copy() @@ -1724,7 +1926,13 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - test_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + test_client = AsyncFinch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) async with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -1748,6 +1956,8 @@ async def test_client_max_retries_validation(self) -> None: AsyncFinch( base_url=base_url, access_token=access_token, + client_id=client_id, + client_secret=client_secret, _strict_response_validation=True, max_retries=cast(Any, None), ) @@ -1759,12 +1969,24 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) + strict_client = AsyncFinch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=True, + ) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - non_strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=False) + non_strict_client = AsyncFinch( + base_url=base_url, + access_token=access_token, + client_id=client_id, + client_secret=client_secret, + _strict_response_validation=False, + ) response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] From b746d12f98b1fcb287bf1b7844fc8a2d864a7d86 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:54:15 +0000 Subject: [PATCH 03/53] chore(internal): codegen related update --- tests/api_resources/hris/test_directory.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/hris/test_directory.py b/tests/api_resources/hris/test_directory.py index fdeca307..dd01b8b9 100644 --- a/tests/api_resources/hris/test_directory.py +++ b/tests/api_resources/hris/test_directory.py @@ -11,6 +11,7 @@ from tests.utils import assert_matches_type from finch.pagination import SyncIndividualsPage, AsyncIndividualsPage from finch.types.hris import IndividualInDirectory +from finch.types.hris.directory_list_individuals_params import UnnamedTypeWithNoPropertyInfoOrParent0 # pyright: reportDeprecated=false @@ -59,7 +60,7 @@ def test_method_list_individuals(self, client: Finch) -> None: with pytest.warns(DeprecationWarning): directory = client.hris.directory.list_individuals() - assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) @parametrize def test_method_list_individuals_with_all_params(self, client: Finch) -> None: @@ -70,7 +71,7 @@ def test_method_list_individuals_with_all_params(self, client: Finch) -> None: offset=0, ) - assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) @parametrize def test_raw_response_list_individuals(self, client: Finch) -> None: @@ -80,7 +81,7 @@ def test_raw_response_list_individuals(self, client: Finch) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" directory = response.parse() - assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) @parametrize def test_streaming_response_list_individuals(self, client: Finch) -> None: @@ -90,7 +91,7 @@ def test_streaming_response_list_individuals(self, client: Finch) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" directory = response.parse() - assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) assert cast(Any, response.is_closed) is True @@ -139,7 +140,7 @@ async def test_method_list_individuals(self, async_client: AsyncFinch) -> None: with pytest.warns(DeprecationWarning): directory = await async_client.hris.directory.list_individuals() - assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) @parametrize async def test_method_list_individuals_with_all_params(self, async_client: AsyncFinch) -> None: @@ -150,7 +151,7 @@ async def test_method_list_individuals_with_all_params(self, async_client: Async offset=0, ) - assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) @parametrize async def test_raw_response_list_individuals(self, async_client: AsyncFinch) -> None: @@ -160,7 +161,7 @@ async def test_raw_response_list_individuals(self, async_client: AsyncFinch) -> assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" directory = response.parse() - assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) @parametrize async def test_streaming_response_list_individuals(self, async_client: AsyncFinch) -> None: @@ -170,6 +171,6 @@ async def test_streaming_response_list_individuals(self, async_client: AsyncFinc assert response.http_request.headers.get("X-Stainless-Lang") == "python" directory = await response.parse() - assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + assert_matches_type(UnnamedTypeWithNoPropertyInfoOrParent0, directory, path=["response"]) assert cast(Any, response.is_closed) is True From e22eb0ffed3e42143600c931f2ab5b342160230c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:34:29 +0000 Subject: [PATCH 04/53] fix(tests): skip broken date validation test --- .stats.yml | 2 +- tests/api_resources/test_access_tokens.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 4db1b6f9..072a0a86 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-46f433f34d440aa1dfcc48cc8d822c598571b68be2f723ec99e1b4fba6c13b1e.yml openapi_spec_hash: 5b5cd728776723ac773900f7e8a32c05 -config_hash: 83522e0e335cf983f8d2119c1f2bba18 +config_hash: ccdf6a5b4aaa2a0897c89ac8685d8eb0 diff --git a/tests/api_resources/test_access_tokens.py b/tests/api_resources/test_access_tokens.py index d71bb756..0dda9602 100644 --- a/tests/api_resources/test_access_tokens.py +++ b/tests/api_resources/test_access_tokens.py @@ -46,6 +46,7 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncFinch]: class TestAccessTokens: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize def test_method_create(self, client: Finch) -> None: access_token = client.access_tokens.create( @@ -53,6 +54,7 @@ def test_method_create(self, client: Finch) -> None: ) assert_matches_type(CreateAccessTokenResponse, access_token, path=["response"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize def test_method_create_with_all_params(self, client: Finch) -> None: access_token = client.access_tokens.create( @@ -63,6 +65,7 @@ def test_method_create_with_all_params(self, client: Finch) -> None: ) assert_matches_type(CreateAccessTokenResponse, access_token, path=["response"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize def test_raw_response_create(self, client: Finch) -> None: response = client.access_tokens.with_raw_response.create( @@ -74,6 +77,7 @@ def test_raw_response_create(self, client: Finch) -> None: access_token = response.parse() assert_matches_type(CreateAccessTokenResponse, access_token, path=["response"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize def test_streaming_response_create(self, client: Finch) -> None: with client.access_tokens.with_streaming_response.create( @@ -91,6 +95,7 @@ def test_streaming_response_create(self, client: Finch) -> None: class TestAsyncAccessTokens: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize async def test_method_create(self, async_client: AsyncFinch) -> None: access_token = await async_client.access_tokens.create( @@ -98,6 +103,7 @@ async def test_method_create(self, async_client: AsyncFinch) -> None: ) assert_matches_type(CreateAccessTokenResponse, access_token, path=["response"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncFinch) -> None: access_token = await async_client.access_tokens.create( @@ -108,6 +114,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncFinch) -> ) assert_matches_type(CreateAccessTokenResponse, access_token, path=["response"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize async def test_raw_response_create(self, async_client: AsyncFinch) -> None: response = await async_client.access_tokens.with_raw_response.create( @@ -119,6 +126,7 @@ async def test_raw_response_create(self, async_client: AsyncFinch) -> None: access_token = response.parse() assert_matches_type(CreateAccessTokenResponse, access_token, path=["response"]) + @pytest.mark.skip(reason="prism doesnt like the format for the API-Version header") @parametrize async def test_streaming_response_create(self, async_client: AsyncFinch) -> None: async with async_client.access_tokens.with_streaming_response.create( From 298ddec8fded9a72146800253d644cb96227cbde Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:36:13 +0000 Subject: [PATCH 05/53] fix(docs): fix mcp installation instructions for remote servers --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70cc1a8b..72f61d2a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Finch MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tryfinch%2Ffinch-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0cnlmaW5jaC9maW5jaC1hcGktbWNwIl19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40tryfinch%2Ffinch-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40tryfinch%2Ffinch-api-mcp%22%5D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tryfinch%2Ffinch-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB0cnlmaW5jaC9maW5jaC1hcGktbWNwIl0sImVudiI6eyJGSU5DSF9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4iLCJGSU5DSF9DTElFTlRfSUQiOiI0YWIxNWU1MS0xMWFkLTQ5ZjQtYWNhZS1mMzQzYjc3OTQzNzUiLCJGSU5DSF9DTElFTlRfU0VDUkVUIjoiTXkgQ2xpZW50IFNlY3JldCIsIkZJTkNIX1dFQkhPT0tfU0VDUkVUIjoiTXkgV2ViaG9vayBTZWNyZXQifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40tryfinch%2Ffinch-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40tryfinch%2Ffinch-api-mcp%22%5D%2C%22env%22%3A%7B%22FINCH_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%2C%22FINCH_CLIENT_ID%22%3A%224ab15e51-11ad-49f4-acae-f343b7794375%22%2C%22FINCH_CLIENT_SECRET%22%3A%22My%20Client%20Secret%22%2C%22FINCH_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 02cb5a0bdcdbd8b4652937928ca7d0b336f6c53c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:57:29 +0000 Subject: [PATCH 06/53] feat(client): add custom JSON encoder for extended type support --- src/finch/_base_client.py | 7 +- src/finch/_compat.py | 6 +- src/finch/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/finch/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/finch/_base_client.py b/src/finch/_base_client.py index 63e36f00..d338a4ce 100644 --- a/src/finch/_base_client.py +++ b/src/finch/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps from ._legacy_response import LegacyAPIResponse log: logging.Logger = logging.getLogger(__name__) @@ -555,8 +556,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/finch/_compat.py b/src/finch/_compat.py index bdef67f0..786ff42a 100644 --- a/src/finch/_compat.py +++ b/src/finch/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/finch/_utils/_json.py b/src/finch/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/finch/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..9db9eaef --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from finch import _compat +from finch._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 0b67a64b36f6824c1000d126e5ed707e495c3c59 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:34:58 +0000 Subject: [PATCH 07/53] chore(internal): bump dependencies --- requirements-dev.lock | 20 ++++++++++---------- requirements.lock | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements-dev.lock b/requirements-dev.lock index 96c9c174..75467572 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,14 +12,14 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via finch-api # via httpx-aiohttp aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via finch-api # via httpx argcomplete==3.6.3 @@ -31,7 +31,7 @@ attrs==25.4.0 # via nox backports-asyncio-runner==1.2.0 # via pytest-asyncio -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx colorlog==6.10.1 @@ -61,7 +61,7 @@ httpx==0.28.1 # via finch-api # via httpx-aiohttp # via respx -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via finch-api humanize==4.13.0 # via nox @@ -69,7 +69,7 @@ idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==8.7.0 +importlib-metadata==8.7.1 iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 @@ -82,14 +82,14 @@ multidict==6.7.0 mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright nox==2025.11.12 packaging==25.0 # via dependency-groups # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy platformdirs==4.4.0 # via virtualenv @@ -115,13 +115,13 @@ python-dateutil==2.9.0.post0 # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 +ruff==0.14.13 six==1.17.0 # via python-dateutil sniffio==1.3.1 # via finch-api time-machine==2.19.0 -tomli==2.3.0 +tomli==2.4.0 # via dependency-groups # via mypy # via nox @@ -141,7 +141,7 @@ typing-extensions==4.15.0 # via virtualenv typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 +virtualenv==20.36.1 # via nox yarl==1.22.0 # via aiohttp diff --git a/requirements.lock b/requirements.lock index dbf05384..a3f1ad70 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,21 +12,21 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.13.2 +aiohttp==3.13.3 # via finch-api # via httpx-aiohttp aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -anyio==4.12.0 +anyio==4.12.1 # via finch-api # via httpx async-timeout==5.0.1 # via aiohttp attrs==25.4.0 # via aiohttp -certifi==2025.11.12 +certifi==2026.1.4 # via httpcore # via httpx distro==1.9.0 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via finch-api # via httpx-aiohttp -httpx-aiohttp==0.1.9 +httpx-aiohttp==0.1.12 # via finch-api idna==3.11 # via anyio From b0f5ba3c117a989524c31e6c62ab9215f98fccd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:22:51 +0000 Subject: [PATCH 08/53] chore(internal): fix lint error on Python 3.14 --- src/finch/_utils/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finch/_utils/_compat.py b/src/finch/_utils/_compat.py index dd703233..2c70b299 100644 --- a/src/finch/_utils/_compat.py +++ b/src/finch/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: From 8555964275d168e400ef190422baee5a1983eeb1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:53:19 +0000 Subject: [PATCH 09/53] chore: format all `api.md` files --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5907bdab..3c5f2867 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ format = { chain = [ # run formatting again to fix any inconsistencies when imports are stripped "format:ruff", ]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" +"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" "format:ruff" = "ruff format" "lint" = { chain = [ From 7891a91c042ae55b9aeaec5980b81197b8692039 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:33:13 +0000 Subject: [PATCH 10/53] chore: update mock server docs --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21f44701..3eb5807c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,8 +88,7 @@ $ pip install ./path-to-wheel-file.whl Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. ```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml +$ ./scripts/mock ``` ```sh From 977dcfa5e7da78f0d4b465843e52a4fd2999799b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:37:03 +0000 Subject: [PATCH 11/53] chore(internal): add request options to SSE classes --- src/finch/_legacy_response.py | 3 +++ src/finch/_response.py | 3 +++ src/finch/_streaming.py | 11 ++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/finch/_legacy_response.py b/src/finch/_legacy_response.py index 24d12a21..a2241252 100644 --- a/src/finch/_legacy_response.py +++ b/src/finch/_legacy_response.py @@ -214,6 +214,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -224,6 +225,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -237,6 +239,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/finch/_response.py b/src/finch/_response.py index 3828f2a2..40ced75f 100644 --- a/src/finch/_response.py +++ b/src/finch/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/finch/_streaming.py b/src/finch/_streaming.py index facc5e66..f4c3580e 100644 --- a/src/finch/_streaming.py +++ b/src/finch/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Finch, AsyncFinch + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Finch, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncFinch, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() From e33584fc7bc4aaaa73377f8436f9858190266236 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:24:33 +0000 Subject: [PATCH 12/53] chore(internal): make `test_proxy_environment_variables` more resilient --- tests/test_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 67ce24a4..6745320a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1089,6 +1089,8 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultHttpxClient() @@ -2131,6 +2133,8 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has this set + monkeypatch.delenv("HTTP_PROXY", raising=False) client = DefaultAsyncHttpxClient() From 9f092c7d989fbe863c8d8624238694c564a871f0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:43:14 +0000 Subject: [PATCH 13/53] chore(internal): make `test_proxy_environment_variables` more resilient to env --- tests/test_client.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 6745320a..144a4cfd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1089,8 +1089,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -2133,8 +2139,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has this set + # Delete in case our environment has any proxy env vars set monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() From 565de8480f61bf07d5dbd0fb39fbb8b4f5ea9671 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:23:57 +0000 Subject: [PATCH 14/53] fix(api): remove invalid transform config --- .stats.yml | 6 +++--- src/finch/types/create_access_token_response.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 072a0a86..0e843e51 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-46f433f34d440aa1dfcc48cc8d822c598571b68be2f723ec99e1b4fba6c13b1e.yml -openapi_spec_hash: 5b5cd728776723ac773900f7e8a32c05 -config_hash: ccdf6a5b4aaa2a0897c89ac8685d8eb0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-c012d034aaa88334d6748526b99a0c0e47b0257c515b35c656749ed8f3720b8a.yml +openapi_spec_hash: a3d3c013ebe997d22e08eea4d487ff03 +config_hash: d21a244fc073152c8dbecb8ece970209 diff --git a/src/finch/types/create_access_token_response.py b/src/finch/types/create_access_token_response.py index 0aacdf48..0edd76d4 100644 --- a/src/finch/types/create_access_token_response.py +++ b/src/finch/types/create_access_token_response.py @@ -54,3 +54,9 @@ class CreateAccessTokenResponse(BaseModel): The ID of your customer you provided to Finch when a connect session was created for this connection """ + + customer_name: Optional[str] = None + """ + The name of your customer you provided to Finch when a connect session was + created for this connection + """ From c6631fc8d74f89931be556746c6ce67e4cb62867 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:40:46 +0000 Subject: [PATCH 15/53] feat(api): api update --- .stats.yml | 4 ++-- src/finch/resources/sandbox/employment.py | 10 ++++++++++ src/finch/types/hris/employment_data.py | 6 ++++++ src/finch/types/sandbox/directory_create_params.py | 6 ++++++ src/finch/types/sandbox/employment_update_params.py | 6 ++++++ src/finch/types/sandbox/employment_update_response.py | 6 ++++++ tests/api_resources/sandbox/test_directory.py | 2 ++ tests/api_resources/sandbox/test_employment.py | 2 ++ 8 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0e843e51..abcde50d 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-c012d034aaa88334d6748526b99a0c0e47b0257c515b35c656749ed8f3720b8a.yml -openapi_spec_hash: a3d3c013ebe997d22e08eea4d487ff03 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-093ade6f1d3115b654a73b97855fbe334c9f9c5d906081dad2ec973ab0c0b24d.yml +openapi_spec_hash: 7cc27b8e483d9db9c411875289c42eb9 config_hash: d21a244fc073152c8dbecb8ece970209 diff --git a/src/finch/resources/sandbox/employment.py b/src/finch/resources/sandbox/employment.py index 8591ced6..386ac281 100644 --- a/src/finch/resources/sandbox/employment.py +++ b/src/finch/resources/sandbox/employment.py @@ -56,6 +56,7 @@ def update( | Omit = omit, end_date: Optional[str] | Omit = omit, first_name: Optional[str] | Omit = omit, + flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] | Omit = omit, income: Optional[IncomeParam] | Omit = omit, income_history: Optional[Iterable[Optional[IncomeParam]]] | Omit = omit, is_active: Optional[bool] | Omit = omit, @@ -92,6 +93,9 @@ def update( first_name: The legal first name of the individual. + flsa_status: The FLSA status of the individual. Available options: `exempt`, `non_exempt`, + `unknown`. + income: The employee's income as reported by the provider. This may not always be annualized income, but may be in units of bi-weekly, semi-monthly, daily, etc, depending on what information the provider returns. @@ -134,6 +138,7 @@ def update( "employment_status": employment_status, "end_date": end_date, "first_name": first_name, + "flsa_status": flsa_status, "income": income, "income_history": income_history, "is_active": is_active, @@ -189,6 +194,7 @@ async def update( | Omit = omit, end_date: Optional[str] | Omit = omit, first_name: Optional[str] | Omit = omit, + flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] | Omit = omit, income: Optional[IncomeParam] | Omit = omit, income_history: Optional[Iterable[Optional[IncomeParam]]] | Omit = omit, is_active: Optional[bool] | Omit = omit, @@ -225,6 +231,9 @@ async def update( first_name: The legal first name of the individual. + flsa_status: The FLSA status of the individual. Available options: `exempt`, `non_exempt`, + `unknown`. + income: The employee's income as reported by the provider. This may not always be annualized income, but may be in units of bi-weekly, semi-monthly, daily, etc, depending on what information the provider returns. @@ -267,6 +276,7 @@ async def update( "employment_status": employment_status, "end_date": end_date, "first_name": first_name, + "flsa_status": flsa_status, "income": income, "income_history": income_history, "is_active": is_active, diff --git a/src/finch/types/hris/employment_data.py b/src/finch/types/hris/employment_data.py index 385eeded..2a9409d6 100644 --- a/src/finch/types/hris/employment_data.py +++ b/src/finch/types/hris/employment_data.py @@ -75,6 +75,12 @@ class UnionMember0(BaseModel): first_name: Optional[str] = None """The legal first name of the individual.""" + flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] = None + """The FLSA status of the individual. + + Available options: `exempt`, `non_exempt`, `unknown`. + """ + is_active: Optional[bool] = None """`true` if the individual an an active employee or contractor at the company.""" diff --git a/src/finch/types/sandbox/directory_create_params.py b/src/finch/types/sandbox/directory_create_params.py index 3c142a74..ce26a93f 100644 --- a/src/finch/types/sandbox/directory_create_params.py +++ b/src/finch/types/sandbox/directory_create_params.py @@ -127,6 +127,12 @@ class Body(TypedDict, total=False): first_name: Optional[str] """The legal first name of the individual.""" + flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] + """The FLSA status of the individual. + + Available options: `exempt`, `non_exempt`, `unknown`. + """ + gender: Optional[Literal["female", "male", "other", "decline_to_specify"]] """The gender of the individual.""" diff --git a/src/finch/types/sandbox/employment_update_params.py b/src/finch/types/sandbox/employment_update_params.py index 1e3469de..21dca764 100644 --- a/src/finch/types/sandbox/employment_update_params.py +++ b/src/finch/types/sandbox/employment_update_params.py @@ -38,6 +38,12 @@ class EmploymentUpdateParams(TypedDict, total=False): first_name: Optional[str] """The legal first name of the individual.""" + flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] + """The FLSA status of the individual. + + Available options: `exempt`, `non_exempt`, `unknown`. + """ + income: Optional[IncomeParam] """The employee's income as reported by the provider. diff --git a/src/finch/types/sandbox/employment_update_response.py b/src/finch/types/sandbox/employment_update_response.py index fb44c946..c349b08d 100644 --- a/src/finch/types/sandbox/employment_update_response.py +++ b/src/finch/types/sandbox/employment_update_response.py @@ -74,6 +74,12 @@ class EmploymentUpdateResponse(BaseModel): first_name: Optional[str] = None """The legal first name of the individual.""" + flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] = None + """The FLSA status of the individual. + + Available options: `exempt`, `non_exempt`, `unknown`. + """ + income_history: Optional[List[Optional[Income]]] = None """The array of income history.""" diff --git a/tests/api_resources/sandbox/test_directory.py b/tests/api_resources/sandbox/test_directory.py index ffc78bec..fcba8f6d 100644 --- a/tests/api_resources/sandbox/test_directory.py +++ b/tests/api_resources/sandbox/test_directory.py @@ -52,6 +52,7 @@ def test_method_create_with_all_params(self, client: Finch) -> None: "end_date": "end_date", "ethnicity": "asian", "first_name": "first_name", + "flsa_status": "exempt", "gender": "female", "income": { "amount": 0, @@ -168,6 +169,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncFinch) -> "end_date": "end_date", "ethnicity": "asian", "first_name": "first_name", + "flsa_status": "exempt", "gender": "female", "income": { "amount": 0, diff --git a/tests/api_resources/sandbox/test_employment.py b/tests/api_resources/sandbox/test_employment.py index c9aecbc0..33595a7f 100644 --- a/tests/api_resources/sandbox/test_employment.py +++ b/tests/api_resources/sandbox/test_employment.py @@ -44,6 +44,7 @@ def test_method_update_with_all_params(self, client: Finch) -> None: employment_status="active", end_date="end_date", first_name="first_name", + flsa_status="exempt", income={ "amount": 0, "currency": "currency", @@ -142,6 +143,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncFinch) -> employment_status="active", end_date="end_date", first_name="first_name", + flsa_status="exempt", income={ "amount": 0, "currency": "currency", From 94aae5c36d464596cda9c899f64be67959fd9cb5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:23:12 +0000 Subject: [PATCH 16/53] chore(internal): codegen related update --- scripts/mock | 13 ++++++- src/finch/_base_client.py | 34 +++++++++++++++---- src/finch/_client.py | 3 +- src/finch/_models.py | 10 ++++++ src/finch/_types.py | 3 +- src/finch/resources/access_tokens.py | 12 +++++-- src/finch/resources/account.py | 28 ++++++++++----- src/finch/resources/connect/sessions.py | 28 ++++++++++----- src/finch/resources/hris/benefits/benefits.py | 20 +++++------ .../resources/hris/benefits/individuals.py | 16 ++++----- src/finch/resources/hris/company/company.py | 4 +-- .../pay_statement_item/pay_statement_item.py | 4 +-- .../hris/company/pay_statement_item/rules.py | 16 ++++----- src/finch/resources/hris/directory.py | 4 +-- src/finch/resources/hris/documents.py | 8 ++--- src/finch/resources/hris/employments.py | 4 +-- src/finch/resources/hris/individuals.py | 4 +-- src/finch/resources/hris/pay_statements.py | 4 +-- src/finch/resources/hris/payments.py | 4 +-- src/finch/resources/jobs/automated.py | 32 +++++++++++------ src/finch/resources/jobs/manual.py | 14 +++++--- src/finch/resources/payroll/pay_groups.py | 8 ++--- src/finch/resources/providers.py | 14 +++++--- src/finch/resources/request_forwarding.py | 14 +++++--- src/finch/resources/sandbox/company.py | 14 +++++--- .../resources/sandbox/connections/accounts.py | 28 ++++++++++----- .../sandbox/connections/connections.py | 14 +++++--- src/finch/resources/sandbox/directory.py | 14 +++++--- src/finch/resources/sandbox/employment.py | 14 +++++--- src/finch/resources/sandbox/individual.py | 14 +++++--- .../resources/sandbox/jobs/configuration.py | 28 ++++++++++----- src/finch/resources/sandbox/jobs/jobs.py | 14 +++++--- src/finch/resources/sandbox/payment.py | 14 +++++--- 33 files changed, 313 insertions(+), 142 deletions(-) diff --git a/scripts/mock b/scripts/mock index 0b28f6ea..bcf3b392 100755 --- a/scripts/mock +++ b/scripts/mock @@ -21,11 +21,22 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - # Wait for server to come online + # Wait for server to come online (max 30s) echo -n "Waiting for server" + attempts=0 while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Prism server to start" + cat .prism.log + exit 1 + fi echo -n "." sleep 0.1 done diff --git a/src/finch/_base_client.py b/src/finch/_base_client.py index d338a4ce..455e3ae1 100644 --- a/src/finch/_base_client.py +++ b/src/finch/_base_client.py @@ -63,7 +63,7 @@ ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump -from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._models import GenericModel, SecurityOptions, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, BaseAPIResponse, @@ -433,9 +433,27 @@ def _make_status_error( ) -> _exceptions.APIStatusError: raise NotImplementedError() + def _auth_headers( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _auth_query( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _custom_auth( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> httpx.Auth | None: + return None + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) + headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers) self._validate_headers(headers_dict, custom_headers) # headers are case-insensitive while dictionaries are not. @@ -507,7 +525,7 @@ def _build_request( raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) + params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params) content_type = headers.get("Content-Type") files = options.files @@ -672,7 +690,6 @@ def default_headers(self) -> dict[str, str | Omit]: "Content-Type": "application/json", "User-Agent": self.user_agent, **self.platform_headers(), - **self.auth_headers, **self._custom_headers, } @@ -991,8 +1008,9 @@ def request( self._prepare_request(request) kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + custom_auth = self._custom_auth(options.security) + if custom_auth is not None: + kwargs["auth"] = custom_auth if options.follow_redirects is not None: kwargs["follow_redirects"] = options.follow_redirects @@ -1981,6 +1999,7 @@ def make_request_options( idempotency_key: str | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, post_parser: PostParser | NotGiven = not_given, + security: SecurityOptions | None = None, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} @@ -2006,6 +2025,9 @@ def make_request_options( # internal options["post_parser"] = post_parser # type: ignore + if security is not None: + options["security"] = security + return options diff --git a/src/finch/_client.py b/src/finch/_client.py index db3ef9e1..4a2f3696 100644 --- a/src/finch/_client.py +++ b/src/finch/_client.py @@ -23,6 +23,7 @@ ) from ._utils import is_given, get_async_library from ._compat import cached_property +from ._models import SecurityOptions from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError @@ -201,7 +202,6 @@ def with_streaming_response(self) -> FinchWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="brackets") - @property @override def auth_headers(self) -> dict[str, str]: if self._bearer_auth: @@ -544,7 +544,6 @@ def with_streaming_response(self) -> AsyncFinchWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="brackets") - @property @override def auth_headers(self) -> dict[str, str]: if self._bearer_auth: diff --git a/src/finch/_models.py b/src/finch/_models.py index 29070e05..1a5474b8 100644 --- a/src/finch/_models.py +++ b/src/finch/_models.py @@ -791,6 +791,11 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: return RootModel[type_] # type: ignore +class SecurityOptions(TypedDict, total=False): + bearer_auth: bool + basic_auth: bool + + class FinalRequestOptionsInput(TypedDict, total=False): method: Required[str] url: Required[str] @@ -804,6 +809,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): json_data: Body extra_json: AnyMapping follow_redirects: bool + security: SecurityOptions @final @@ -818,6 +824,10 @@ class FinalRequestOptions(pydantic.BaseModel): idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + security: SecurityOptions = { + "bearer_auth": True, + "basic_auth": True, + } content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override diff --git a/src/finch/_types.py b/src/finch/_types.py index 0a51d1b7..705f9a00 100644 --- a/src/finch/_types.py +++ b/src/finch/_types.py @@ -36,7 +36,7 @@ from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport if TYPE_CHECKING: - from ._models import BaseModel + from ._models import BaseModel, SecurityOptions from ._response import APIResponse, AsyncAPIResponse from ._legacy_response import HttpxBinaryResponseContent @@ -122,6 +122,7 @@ class RequestOptions(TypedDict, total=False): extra_json: AnyMapping idempotency_key: str follow_redirects: bool + security: SecurityOptions # Sentinel class used until PEP 0661 is accepted diff --git a/src/finch/resources/access_tokens.py b/src/finch/resources/access_tokens.py index f1f46034..66c29dfa 100644 --- a/src/finch/resources/access_tokens.py +++ b/src/finch/resources/access_tokens.py @@ -99,7 +99,11 @@ def create( access_token_create_params.AccessTokenCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={}, ), cast_to=CreateAccessTokenResponse, ) @@ -187,7 +191,11 @@ async def create( access_token_create_params.AccessTokenCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={}, ), cast_to=CreateAccessTokenResponse, ) diff --git a/src/finch/resources/account.py b/src/finch/resources/account.py index ac8583a2..86e025c4 100644 --- a/src/finch/resources/account.py +++ b/src/finch/resources/account.py @@ -47,11 +47,14 @@ def disconnect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DisconnectResponse: """Disconnect one or more `access_token`s from your application.""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/disconnect", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=DisconnectResponse, ) @@ -67,11 +70,14 @@ def introspect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Introspection: """Read account information associated with an `access_token`""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/introspect", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=Introspection, ) @@ -108,11 +114,14 @@ async def disconnect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> DisconnectResponse: """Disconnect one or more `access_token`s from your application.""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/disconnect", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=DisconnectResponse, ) @@ -128,11 +137,14 @@ async def introspect( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Introspection: """Read account information associated with an `access_token`""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/introspect", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=Introspection, ) diff --git a/src/finch/resources/connect/sessions.py b/src/finch/resources/connect/sessions.py index 648deb68..a89f5b15 100644 --- a/src/finch/resources/connect/sessions.py +++ b/src/finch/resources/connect/sessions.py @@ -104,7 +104,6 @@ def new( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/connect/sessions", body=maybe_transform( @@ -122,7 +121,11 @@ def new( session_new_params.SessionNewParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=SessionNewResponse, ) @@ -178,7 +181,6 @@ def reauthenticate( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/connect/sessions/reauthenticate", body=maybe_transform( @@ -191,7 +193,11 @@ def reauthenticate( session_reauthenticate_params.SessionReauthenticateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=SessionReauthenticateResponse, ) @@ -280,7 +286,6 @@ async def new( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/connect/sessions", body=await async_maybe_transform( @@ -298,7 +303,11 @@ async def new( session_new_params.SessionNewParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=SessionNewResponse, ) @@ -354,7 +363,6 @@ async def reauthenticate( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/connect/sessions/reauthenticate", body=await async_maybe_transform( @@ -367,7 +375,11 @@ async def reauthenticate( session_reauthenticate_params.SessionReauthenticateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=SessionReauthenticateResponse, ) diff --git a/src/finch/resources/hris/benefits/benefits.py b/src/finch/resources/hris/benefits/benefits.py index 91236b33..dd6d6e86 100644 --- a/src/finch/resources/hris/benefits/benefits.py +++ b/src/finch/resources/hris/benefits/benefits.py @@ -106,7 +106,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/employer/benefits", body=maybe_transform( @@ -124,6 +123,7 @@ def create( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, benefit_create_params.BenefitCreateParams), + security={"bearer_auth": True}, ), cast_to=CreateCompanyBenefitsResponse, ) @@ -156,7 +156,6 @@ def retrieve( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/employer/benefits/{benefit_id}", options=make_request_options( @@ -165,6 +164,7 @@ def retrieve( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, benefit_retrieve_params.BenefitRetrieveParams), + security={"bearer_auth": True}, ), cast_to=CompanyBenefit, ) @@ -200,7 +200,6 @@ def update( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( f"/employer/benefits/{benefit_id}", body=maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams), @@ -210,6 +209,7 @@ def update( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, benefit_update_params.BenefitUpdateParams), + security={"bearer_auth": True}, ), cast_to=UpdateCompanyBenefitResponse, ) @@ -239,7 +239,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits", page=SyncSinglePage[CompanyBenefit], @@ -249,6 +248,7 @@ def list( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, benefit_list_params.BenefitListParams), + security={"bearer_auth": True}, ), model=CompanyBenefit, ) @@ -278,7 +278,6 @@ def list_supported_benefits( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits/meta", page=SyncSinglePage[SupportedBenefit], @@ -291,6 +290,7 @@ def list_supported_benefits( {"entity_ids": entity_ids}, benefit_list_supported_benefits_params.BenefitListSupportedBenefitsParams, ), + security={"bearer_auth": True}, ), model=SupportedBenefit, ) @@ -361,7 +361,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/employer/benefits", body=await async_maybe_transform( @@ -381,6 +380,7 @@ async def create( query=await async_maybe_transform( {"entity_ids": entity_ids}, benefit_create_params.BenefitCreateParams ), + security={"bearer_auth": True}, ), cast_to=CreateCompanyBenefitsResponse, ) @@ -413,7 +413,6 @@ async def retrieve( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/employer/benefits/{benefit_id}", options=make_request_options( @@ -424,6 +423,7 @@ async def retrieve( query=await async_maybe_transform( {"entity_ids": entity_ids}, benefit_retrieve_params.BenefitRetrieveParams ), + security={"bearer_auth": True}, ), cast_to=CompanyBenefit, ) @@ -459,7 +459,6 @@ async def update( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( f"/employer/benefits/{benefit_id}", body=await async_maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams), @@ -471,6 +470,7 @@ async def update( query=await async_maybe_transform( {"entity_ids": entity_ids}, benefit_update_params.BenefitUpdateParams ), + security={"bearer_auth": True}, ), cast_to=UpdateCompanyBenefitResponse, ) @@ -500,7 +500,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits", page=AsyncSinglePage[CompanyBenefit], @@ -510,6 +509,7 @@ def list( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, benefit_list_params.BenefitListParams), + security={"bearer_auth": True}, ), model=CompanyBenefit, ) @@ -539,7 +539,6 @@ def list_supported_benefits( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/benefits/meta", page=AsyncSinglePage[SupportedBenefit], @@ -552,6 +551,7 @@ def list_supported_benefits( {"entity_ids": entity_ids}, benefit_list_supported_benefits_params.BenefitListSupportedBenefitsParams, ), + security={"bearer_auth": True}, ), model=SupportedBenefit, ) diff --git a/src/finch/resources/hris/benefits/individuals.py b/src/finch/resources/hris/benefits/individuals.py index c03f86fb..0d9906de 100644 --- a/src/finch/resources/hris/benefits/individuals.py +++ b/src/finch/resources/hris/benefits/individuals.py @@ -83,7 +83,6 @@ def enroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( f"/employer/benefits/{benefit_id}/individuals", body=maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), @@ -95,6 +94,7 @@ def enroll_many( query=maybe_transform( {"entity_ids": entity_ids}, individual_enroll_many_params.IndividualEnrollManyParams ), + security={"bearer_auth": True}, ), cast_to=EnrolledIndividualBenefitResponse, ) @@ -127,7 +127,6 @@ def enrolled_ids( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/employer/benefits/{benefit_id}/enrolled", options=make_request_options( @@ -138,6 +137,7 @@ def enrolled_ids( query=maybe_transform( {"entity_ids": entity_ids}, individual_enrolled_ids_params.IndividualEnrolledIDsParams ), + security={"bearer_auth": True}, ), cast_to=IndividualEnrolledIDsResponse, ) @@ -174,7 +174,6 @@ def retrieve_many_benefits( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( f"/employer/benefits/{benefit_id}/individuals", page=SyncSinglePage[IndividualBenefit], @@ -190,6 +189,7 @@ def retrieve_many_benefits( }, individual_retrieve_many_benefits_params.IndividualRetrieveManyBenefitsParams, ), + security={"bearer_auth": True}, ), model=IndividualBenefit, ) @@ -225,7 +225,6 @@ def unenroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._delete( f"/employer/benefits/{benefit_id}/individuals", body=maybe_transform( @@ -239,6 +238,7 @@ def unenroll_many( query=maybe_transform( {"entity_ids": entity_ids}, individual_unenroll_many_params.IndividualUnenrollManyParams ), + security={"bearer_auth": True}, ), cast_to=UnenrolledIndividualBenefitResponse, ) @@ -299,7 +299,6 @@ async def enroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( f"/employer/benefits/{benefit_id}/individuals", body=await async_maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), @@ -311,6 +310,7 @@ async def enroll_many( query=await async_maybe_transform( {"entity_ids": entity_ids}, individual_enroll_many_params.IndividualEnrollManyParams ), + security={"bearer_auth": True}, ), cast_to=EnrolledIndividualBenefitResponse, ) @@ -343,7 +343,6 @@ async def enrolled_ids( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/employer/benefits/{benefit_id}/enrolled", options=make_request_options( @@ -354,6 +353,7 @@ async def enrolled_ids( query=await async_maybe_transform( {"entity_ids": entity_ids}, individual_enrolled_ids_params.IndividualEnrolledIDsParams ), + security={"bearer_auth": True}, ), cast_to=IndividualEnrolledIDsResponse, ) @@ -390,7 +390,6 @@ def retrieve_many_benefits( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( f"/employer/benefits/{benefit_id}/individuals", page=AsyncSinglePage[IndividualBenefit], @@ -406,6 +405,7 @@ def retrieve_many_benefits( }, individual_retrieve_many_benefits_params.IndividualRetrieveManyBenefitsParams, ), + security={"bearer_auth": True}, ), model=IndividualBenefit, ) @@ -441,7 +441,6 @@ async def unenroll_many( """ if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._delete( f"/employer/benefits/{benefit_id}/individuals", body=await async_maybe_transform( @@ -455,6 +454,7 @@ async def unenroll_many( query=await async_maybe_transform( {"entity_ids": entity_ids}, individual_unenroll_many_params.IndividualUnenrollManyParams ), + security={"bearer_auth": True}, ), cast_to=UnenrolledIndividualBenefitResponse, ) diff --git a/src/finch/resources/hris/company/company.py b/src/finch/resources/hris/company/company.py index 1bc042b7..f18c4cfd 100644 --- a/src/finch/resources/hris/company/company.py +++ b/src/finch/resources/hris/company/company.py @@ -74,7 +74,6 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/employer/company", options=make_request_options( @@ -83,6 +82,7 @@ def retrieve( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, company_retrieve_params.CompanyRetrieveParams), + security={"bearer_auth": True}, ), cast_to=Company, ) @@ -137,7 +137,6 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/employer/company", options=make_request_options( @@ -148,6 +147,7 @@ async def retrieve( query=await async_maybe_transform( {"entity_ids": entity_ids}, company_retrieve_params.CompanyRetrieveParams ), + security={"bearer_auth": True}, ), cast_to=Company, ) diff --git a/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py b/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py index 5254872d..57686bc6 100644 --- a/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py +++ b/src/finch/resources/hris/company/pay_statement_item/pay_statement_item.py @@ -98,7 +98,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item", page=SyncResponsesPage[PayStatementItemListResponse], @@ -118,6 +117,7 @@ def list( }, pay_statement_item_list_params.PayStatementItemListParams, ), + security={"bearer_auth": True}, ), model=PayStatementItemListResponse, ) @@ -191,7 +191,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item", page=AsyncResponsesPage[PayStatementItemListResponse], @@ -211,6 +210,7 @@ def list( }, pay_statement_item_list_params.PayStatementItemListParams, ), + security={"bearer_auth": True}, ), model=PayStatementItemListResponse, ) diff --git a/src/finch/resources/hris/company/pay_statement_item/rules.py b/src/finch/resources/hris/company/pay_statement_item/rules.py index 7fc7d9ad..55e44523 100644 --- a/src/finch/resources/hris/company/pay_statement_item/rules.py +++ b/src/finch/resources/hris/company/pay_statement_item/rules.py @@ -90,7 +90,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/employer/pay-statement-item/rule", body=maybe_transform( @@ -109,6 +108,7 @@ def create( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, rule_create_params.RuleCreateParams), + security={"bearer_auth": True}, ), cast_to=RuleCreateResponse, ) @@ -142,7 +142,6 @@ def update( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( f"/employer/pay-statement-item/rule/{rule_id}", body=maybe_transform({"optional_property": optional_property}, rule_update_params.RuleUpdateParams), @@ -152,6 +151,7 @@ def update( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, rule_update_params.RuleUpdateParams), + security={"bearer_auth": True}, ), cast_to=RuleUpdateResponse, ) @@ -181,7 +181,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item/rule", page=SyncResponsesPage[RuleListResponse], @@ -191,6 +190,7 @@ def list( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, rule_list_params.RuleListParams), + security={"bearer_auth": True}, ), model=RuleListResponse, ) @@ -223,7 +223,6 @@ def delete( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._delete( f"/employer/pay-statement-item/rule/{rule_id}", options=make_request_options( @@ -232,6 +231,7 @@ def delete( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, rule_delete_params.RuleDeleteParams), + security={"bearer_auth": True}, ), cast_to=RuleDeleteResponse, ) @@ -298,7 +298,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/employer/pay-statement-item/rule", body=await async_maybe_transform( @@ -317,6 +316,7 @@ async def create( extra_body=extra_body, timeout=timeout, query=await async_maybe_transform({"entity_ids": entity_ids}, rule_create_params.RuleCreateParams), + security={"bearer_auth": True}, ), cast_to=RuleCreateResponse, ) @@ -350,7 +350,6 @@ async def update( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( f"/employer/pay-statement-item/rule/{rule_id}", body=await async_maybe_transform( @@ -362,6 +361,7 @@ async def update( extra_body=extra_body, timeout=timeout, query=await async_maybe_transform({"entity_ids": entity_ids}, rule_update_params.RuleUpdateParams), + security={"bearer_auth": True}, ), cast_to=RuleUpdateResponse, ) @@ -391,7 +391,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement-item/rule", page=AsyncResponsesPage[RuleListResponse], @@ -401,6 +400,7 @@ def list( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, rule_list_params.RuleListParams), + security={"bearer_auth": True}, ), model=RuleListResponse, ) @@ -433,7 +433,6 @@ async def delete( """ if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._delete( f"/employer/pay-statement-item/rule/{rule_id}", options=make_request_options( @@ -442,6 +441,7 @@ async def delete( extra_body=extra_body, timeout=timeout, query=await async_maybe_transform({"entity_ids": entity_ids}, rule_delete_params.RuleDeleteParams), + security={"bearer_auth": True}, ), cast_to=RuleDeleteResponse, ) diff --git a/src/finch/resources/hris/directory.py b/src/finch/resources/hris/directory.py index 50cbae78..e3ea97e0 100644 --- a/src/finch/resources/hris/directory.py +++ b/src/finch/resources/hris/directory.py @@ -71,7 +71,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/directory", page=SyncIndividualsPage[IndividualInDirectory], @@ -88,6 +87,7 @@ def list( }, directory_list_params.DirectoryListParams, ), + security={"bearer_auth": True}, ), model=IndividualInDirectory, ) @@ -186,7 +186,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/directory", page=AsyncIndividualsPage[IndividualInDirectory], @@ -203,6 +202,7 @@ def list( }, directory_list_params.DirectoryListParams, ), + security={"bearer_auth": True}, ), model=IndividualInDirectory, ) diff --git a/src/finch/resources/hris/documents.py b/src/finch/resources/hris/documents.py index 5c56765a..ec32f30e 100644 --- a/src/finch/resources/hris/documents.py +++ b/src/finch/resources/hris/documents.py @@ -82,7 +82,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/employer/documents", options=make_request_options( @@ -100,6 +99,7 @@ def list( }, document_list_params.DocumentListParams, ), + security={"bearer_auth": True}, ), cast_to=DocumentListResponse, ) @@ -134,7 +134,6 @@ def retreive( """ if not document_id: raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return cast( DocumentRetreiveResponse, self._get( @@ -145,6 +144,7 @@ def retreive( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, document_retreive_params.DocumentRetreiveParams), + security={"bearer_auth": True}, ), cast_to=cast( Any, DocumentRetreiveResponse @@ -214,7 +214,6 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/employer/documents", options=make_request_options( @@ -232,6 +231,7 @@ async def list( }, document_list_params.DocumentListParams, ), + security={"bearer_auth": True}, ), cast_to=DocumentListResponse, ) @@ -266,7 +266,6 @@ async def retreive( """ if not document_id: raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return cast( DocumentRetreiveResponse, await self._get( @@ -279,6 +278,7 @@ async def retreive( query=await async_maybe_transform( {"entity_ids": entity_ids}, document_retreive_params.DocumentRetreiveParams ), + security={"bearer_auth": True}, ), cast_to=cast( Any, DocumentRetreiveResponse diff --git a/src/finch/resources/hris/employments.py b/src/finch/resources/hris/employments.py index 07d3087b..65aaa4e0 100644 --- a/src/finch/resources/hris/employments.py +++ b/src/finch/resources/hris/employments.py @@ -68,7 +68,6 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/employment", page=SyncResponsesPage[EmploymentDataResponse], @@ -81,6 +80,7 @@ def retrieve_many( query=maybe_transform( {"entity_ids": entity_ids}, employment_retrieve_many_params.EmploymentRetrieveManyParams ), + security={"bearer_auth": True}, ), model=EmploymentDataResponse, method="post", @@ -135,7 +135,6 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/employment", page=AsyncResponsesPage[EmploymentDataResponse], @@ -148,6 +147,7 @@ def retrieve_many( query=maybe_transform( {"entity_ids": entity_ids}, employment_retrieve_many_params.EmploymentRetrieveManyParams ), + security={"bearer_auth": True}, ), model=EmploymentDataResponse, method="post", diff --git a/src/finch/resources/hris/individuals.py b/src/finch/resources/hris/individuals.py index 64f04179..abe08b9c 100644 --- a/src/finch/resources/hris/individuals.py +++ b/src/finch/resources/hris/individuals.py @@ -67,7 +67,6 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/individual", page=SyncResponsesPage[IndividualResponse], @@ -86,6 +85,7 @@ def retrieve_many( query=maybe_transform( {"entity_ids": entity_ids}, individual_retrieve_many_params.IndividualRetrieveManyParams ), + security={"bearer_auth": True}, ), model=IndividualResponse, method="post", @@ -139,7 +139,6 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/individual", page=AsyncResponsesPage[IndividualResponse], @@ -158,6 +157,7 @@ def retrieve_many( query=maybe_transform( {"entity_ids": entity_ids}, individual_retrieve_many_params.IndividualRetrieveManyParams ), + security={"bearer_auth": True}, ), model=IndividualResponse, method="post", diff --git a/src/finch/resources/hris/pay_statements.py b/src/finch/resources/hris/pay_statements.py index 64312286..fc424b01 100644 --- a/src/finch/resources/hris/pay_statements.py +++ b/src/finch/resources/hris/pay_statements.py @@ -71,7 +71,6 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement", page=SyncResponsesPage[PayStatementResponse], @@ -86,6 +85,7 @@ def retrieve_many( query=maybe_transform( {"entity_ids": entity_ids}, pay_statement_retrieve_many_params.PayStatementRetrieveManyParams ), + security={"bearer_auth": True}, ), model=PayStatementResponse, method="post", @@ -143,7 +143,6 @@ def retrieve_many( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-statement", page=AsyncResponsesPage[PayStatementResponse], @@ -158,6 +157,7 @@ def retrieve_many( query=maybe_transform( {"entity_ids": entity_ids}, pay_statement_retrieve_many_params.PayStatementRetrieveManyParams ), + security={"bearer_auth": True}, ), model=PayStatementResponse, method="post", diff --git a/src/finch/resources/hris/payments.py b/src/finch/resources/hris/payments.py index 2b226949..99a5e5a9 100644 --- a/src/finch/resources/hris/payments.py +++ b/src/finch/resources/hris/payments.py @@ -74,7 +74,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/payment", page=SyncSinglePage[Payment], @@ -91,6 +90,7 @@ def list( }, payment_list_params.PaymentListParams, ), + security={"bearer_auth": True}, ), model=Payment, ) @@ -149,7 +149,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/payment", page=AsyncSinglePage[Payment], @@ -166,6 +165,7 @@ def list( }, payment_list_params.PaymentListParams, ), + security={"bearer_auth": True}, ), model=Payment, ) diff --git a/src/finch/resources/jobs/automated.py b/src/finch/resources/jobs/automated.py index fa266ac7..161c6335 100644 --- a/src/finch/resources/jobs/automated.py +++ b/src/finch/resources/jobs/automated.py @@ -137,7 +137,6 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AutomatedCreateResponse: - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/jobs/automated", body=maybe_transform( @@ -148,7 +147,11 @@ def create( automated_create_params.AutomatedCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=AutomatedCreateResponse, ) @@ -178,11 +181,14 @@ def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/jobs/automated/{job_id}", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=AutomatedAsyncJob, ) @@ -218,7 +224,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/jobs/automated", options=make_request_options( @@ -233,6 +238,7 @@ def list( }, automated_list_params.AutomatedListParams, ), + security={"bearer_auth": True}, ), cast_to=AutomatedListResponse, ) @@ -354,7 +360,6 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AutomatedCreateResponse: - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/jobs/automated", body=await async_maybe_transform( @@ -365,7 +370,11 @@ async def create( automated_create_params.AutomatedCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=AutomatedCreateResponse, ) @@ -395,11 +404,14 @@ async def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/jobs/automated/{job_id}", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=AutomatedAsyncJob, ) @@ -435,7 +447,6 @@ async def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/jobs/automated", options=make_request_options( @@ -450,6 +461,7 @@ async def list( }, automated_list_params.AutomatedListParams, ), + security={"bearer_auth": True}, ), cast_to=AutomatedListResponse, ) diff --git a/src/finch/resources/jobs/manual.py b/src/finch/resources/jobs/manual.py index d391d137..596cda37 100644 --- a/src/finch/resources/jobs/manual.py +++ b/src/finch/resources/jobs/manual.py @@ -62,11 +62,14 @@ def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/jobs/manual/{job_id}", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=ManualAsyncJob, ) @@ -119,11 +122,14 @@ async def retrieve( """ if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/jobs/manual/{job_id}", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=ManualAsyncJob, ) diff --git a/src/finch/resources/payroll/pay_groups.py b/src/finch/resources/payroll/pay_groups.py index 91c5edbf..d4fe550f 100644 --- a/src/finch/resources/payroll/pay_groups.py +++ b/src/finch/resources/payroll/pay_groups.py @@ -67,7 +67,6 @@ def retrieve( """ if not pay_group_id: raise ValueError(f"Expected a non-empty value for `pay_group_id` but received {pay_group_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( f"/employer/pay-groups/{pay_group_id}", options=make_request_options( @@ -76,6 +75,7 @@ def retrieve( extra_body=extra_body, timeout=timeout, query=maybe_transform({"entity_ids": entity_ids}, pay_group_retrieve_params.PayGroupRetrieveParams), + security={"bearer_auth": True}, ), cast_to=PayGroupRetrieveResponse, ) @@ -107,7 +107,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-groups", page=SyncSinglePage[PayGroupListResponse], @@ -124,6 +123,7 @@ def list( }, pay_group_list_params.PayGroupListParams, ), + security={"bearer_auth": True}, ), model=PayGroupListResponse, ) @@ -177,7 +177,6 @@ async def retrieve( """ if not pay_group_id: raise ValueError(f"Expected a non-empty value for `pay_group_id` but received {pay_group_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( f"/employer/pay-groups/{pay_group_id}", options=make_request_options( @@ -188,6 +187,7 @@ async def retrieve( query=await async_maybe_transform( {"entity_ids": entity_ids}, pay_group_retrieve_params.PayGroupRetrieveParams ), + security={"bearer_auth": True}, ), cast_to=PayGroupRetrieveResponse, ) @@ -219,7 +219,6 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/employer/pay-groups", page=AsyncSinglePage[PayGroupListResponse], @@ -236,6 +235,7 @@ def list( }, pay_group_list_params.PayGroupListParams, ), + security={"bearer_auth": True}, ), model=PayGroupListResponse, ) diff --git a/src/finch/resources/providers.py b/src/finch/resources/providers.py index 2e37ff56..c0eb5699 100644 --- a/src/finch/resources/providers.py +++ b/src/finch/resources/providers.py @@ -47,12 +47,15 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncSinglePage[ProviderListResponse]: """Return details on all available payroll and HR systems.""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/providers", page=SyncSinglePage[ProviderListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), model=ProviderListResponse, ) @@ -89,12 +92,15 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[ProviderListResponse, AsyncSinglePage[ProviderListResponse]]: """Return details on all available payroll and HR systems.""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get_api_list( "/providers", page=AsyncSinglePage[ProviderListResponse], options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), model=ProviderListResponse, ) diff --git a/src/finch/resources/request_forwarding.py b/src/finch/resources/request_forwarding.py index 01bea5a8..7366a856 100644 --- a/src/finch/resources/request_forwarding.py +++ b/src/finch/resources/request_forwarding.py @@ -87,7 +87,6 @@ def forward( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/forward", body=maybe_transform( @@ -101,7 +100,11 @@ def forward( request_forwarding_forward_params.RequestForwardingForwardParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=RequestForwardingForwardResponse, ) @@ -175,7 +178,6 @@ async def forward( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/forward", body=await async_maybe_transform( @@ -189,7 +191,11 @@ async def forward( request_forwarding_forward_params.RequestForwardingForwardParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=RequestForwardingForwardResponse, ) diff --git a/src/finch/resources/sandbox/company.py b/src/finch/resources/sandbox/company.py index 714bc7b1..4e13f998 100644 --- a/src/finch/resources/sandbox/company.py +++ b/src/finch/resources/sandbox/company.py @@ -85,7 +85,6 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( "/sandbox/company", body=maybe_transform( @@ -102,7 +101,11 @@ def update( company_update_params.CompanyUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=CompanyUpdateResponse, ) @@ -173,7 +176,6 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( "/sandbox/company", body=await async_maybe_transform( @@ -190,7 +192,11 @@ async def update( company_update_params.CompanyUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=CompanyUpdateResponse, ) diff --git a/src/finch/resources/sandbox/connections/accounts.py b/src/finch/resources/sandbox/connections/accounts.py index e8d168b9..046beb43 100644 --- a/src/finch/resources/sandbox/connections/accounts.py +++ b/src/finch/resources/sandbox/connections/accounts.py @@ -72,7 +72,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/sandbox/connections/accounts", body=maybe_transform( @@ -85,7 +84,11 @@ def create( account_create_params.AccountCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=AccountCreateResponse, ) @@ -115,12 +118,15 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( "/sandbox/connections/accounts", body=maybe_transform({"connection_status": connection_status}, account_update_params.AccountUpdateParams), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=AccountUpdateResponse, ) @@ -177,7 +183,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/sandbox/connections/accounts", body=await async_maybe_transform( @@ -190,7 +195,11 @@ async def create( account_create_params.AccountCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=AccountCreateResponse, ) @@ -220,14 +229,17 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( "/sandbox/connections/accounts", body=await async_maybe_transform( {"connection_status": connection_status}, account_update_params.AccountUpdateParams ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=AccountUpdateResponse, ) diff --git a/src/finch/resources/sandbox/connections/connections.py b/src/finch/resources/sandbox/connections/connections.py index a3f0c4b1..a99c22f7 100644 --- a/src/finch/resources/sandbox/connections/connections.py +++ b/src/finch/resources/sandbox/connections/connections.py @@ -83,7 +83,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return self._post( "/sandbox/connections", body=maybe_transform( @@ -96,7 +95,11 @@ def create( connection_create_params.ConnectionCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=ConnectionCreateResponse, ) @@ -158,7 +161,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._basic_auth, **(extra_headers or {})} return await self._post( "/sandbox/connections", body=await async_maybe_transform( @@ -171,7 +173,11 @@ async def create( connection_create_params.ConnectionCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"basic_auth": True}, ), cast_to=ConnectionCreateResponse, ) diff --git a/src/finch/resources/sandbox/directory.py b/src/finch/resources/sandbox/directory.py index 6096fdc0..b1b5f4cf 100644 --- a/src/finch/resources/sandbox/directory.py +++ b/src/finch/resources/sandbox/directory.py @@ -65,12 +65,15 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/sandbox/directory", 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 + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=DirectoryCreateResponse, ) @@ -122,12 +125,15 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/sandbox/directory", 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 + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=DirectoryCreateResponse, ) diff --git a/src/finch/resources/sandbox/employment.py b/src/finch/resources/sandbox/employment.py index 386ac281..1db00690 100644 --- a/src/finch/resources/sandbox/employment.py +++ b/src/finch/resources/sandbox/employment.py @@ -126,7 +126,6 @@ def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( f"/sandbox/employment/{individual_id}", body=maybe_transform( @@ -154,7 +153,11 @@ def update( employment_update_params.EmploymentUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=EmploymentUpdateResponse, ) @@ -264,7 +267,6 @@ async def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( f"/sandbox/employment/{individual_id}", body=await async_maybe_transform( @@ -292,7 +294,11 @@ async def update( employment_update_params.EmploymentUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=EmploymentUpdateResponse, ) diff --git a/src/finch/resources/sandbox/individual.py b/src/finch/resources/sandbox/individual.py index 399a88c2..a206dc7f 100644 --- a/src/finch/resources/sandbox/individual.py +++ b/src/finch/resources/sandbox/individual.py @@ -113,7 +113,6 @@ def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( f"/sandbox/individual/{individual_id}", body=maybe_transform( @@ -134,7 +133,11 @@ def update( individual_update_params.IndividualUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=IndividualUpdateResponse, ) @@ -232,7 +235,6 @@ async def update( """ if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( f"/sandbox/individual/{individual_id}", body=await async_maybe_transform( @@ -253,7 +255,11 @@ async def update( individual_update_params.IndividualUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=IndividualUpdateResponse, ) diff --git a/src/finch/resources/sandbox/jobs/configuration.py b/src/finch/resources/sandbox/jobs/configuration.py index e3239cc6..0c767a17 100644 --- a/src/finch/resources/sandbox/jobs/configuration.py +++ b/src/finch/resources/sandbox/jobs/configuration.py @@ -51,11 +51,14 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ConfigurationRetrieveResponse: """Get configurations for sandbox jobs""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._get( "/sandbox/jobs/configuration", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=ConfigurationRetrieveResponse, ) @@ -84,7 +87,6 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._put( "/sandbox/jobs/configuration", body=maybe_transform( @@ -95,7 +97,11 @@ def update( configuration_update_params.ConfigurationUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=SandboxJobConfiguration, ) @@ -132,11 +138,14 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ConfigurationRetrieveResponse: """Get configurations for sandbox jobs""" - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._get( "/sandbox/jobs/configuration", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=ConfigurationRetrieveResponse, ) @@ -165,7 +174,6 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._put( "/sandbox/jobs/configuration", body=await async_maybe_transform( @@ -176,7 +184,11 @@ async def update( configuration_update_params.ConfigurationUpdateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=SandboxJobConfiguration, ) diff --git a/src/finch/resources/sandbox/jobs/jobs.py b/src/finch/resources/sandbox/jobs/jobs.py index 4ac88a37..580786a3 100644 --- a/src/finch/resources/sandbox/jobs/jobs.py +++ b/src/finch/resources/sandbox/jobs/jobs.py @@ -77,12 +77,15 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/sandbox/jobs", body=maybe_transform({"type": type}, job_create_params.JobCreateParams), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=JobCreateResponse, ) @@ -138,12 +141,15 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/sandbox/jobs", body=await async_maybe_transform({"type": type}, job_create_params.JobCreateParams), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=JobCreateResponse, ) diff --git a/src/finch/resources/sandbox/payment.py b/src/finch/resources/sandbox/payment.py index f08880a7..8aba9fbf 100644 --- a/src/finch/resources/sandbox/payment.py +++ b/src/finch/resources/sandbox/payment.py @@ -67,7 +67,6 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return self._post( "/sandbox/payment", body=maybe_transform( @@ -79,7 +78,11 @@ def create( payment_create_params.PaymentCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=PaymentCreateResponse, ) @@ -132,7 +135,6 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {**self._client._bearer_auth, **(extra_headers or {})} return await self._post( "/sandbox/payment", body=await async_maybe_transform( @@ -144,7 +146,11 @@ async def create( payment_create_params.PaymentCreateParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={"bearer_auth": True}, ), cast_to=PaymentCreateResponse, ) From 0d13335d29a546c8cc94e67b59c19835821f974e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:05:14 +0000 Subject: [PATCH 17/53] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a0c69c5..2a269aa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,14 +61,18 @@ jobs: run: rye build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/finch-python' + if: |- + github.repository == 'stainless-sdks/finch-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/finch-python' + if: |- + github.repository == 'stainless-sdks/finch-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From 399bc94cf48e0518f87b239ead5e869d66b03fce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:36:05 +0000 Subject: [PATCH 18/53] fix(pydantic): do not pass `by_alias` unless set --- src/finch/_compat.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/finch/_compat.py b/src/finch/_compat.py index 786ff42a..e6690a4f 100644 --- a/src/finch/_compat.py +++ b/src/finch/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -142,6 +146,9 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -149,7 +156,7 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, + **kwargs, ) return cast( "dict[str, Any]", From 16af387fd95f0207db3ce4a072cab0946ce977af Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:05:49 +0000 Subject: [PATCH 19/53] fix(deps): bump minimum typing-extensions version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3c5f2867..09b77674 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", From 9e540f1c1e03cd4099b4b7338fc689c569bd8e54 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:47:47 +0000 Subject: [PATCH 20/53] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a269aa0..bb48c743 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From 077413765398e76855de34eec1e26e899f9ded8a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:48:29 +0000 Subject: [PATCH 21/53] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index abcde50d..db08ac03 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-093ade6f1d3115b654a73b97855fbe334c9f9c5d906081dad2ec973ab0c0b24d.yml -openapi_spec_hash: 7cc27b8e483d9db9c411875289c42eb9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-51b4b982d0bd629dbfe3e78a4a41f52154506a7a259f815f29e3ad402704b426.yml +openapi_spec_hash: 800e70018b89117b79b4c6628f6773f2 config_hash: d21a244fc073152c8dbecb8ece970209 From 896338f100cb035ad2ff29d9c7f199836aafe85c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:40:21 +0000 Subject: [PATCH 22/53] fix: sanitize endpoint path params --- src/finch/_utils/__init__.py | 1 + src/finch/_utils/_path.py | 127 ++++++++++++++++++ src/finch/resources/hris/benefits/benefits.py | 10 +- .../resources/hris/benefits/individuals.py | 18 +-- .../hris/company/pay_statement_item/rules.py | 10 +- src/finch/resources/hris/documents.py | 6 +- src/finch/resources/jobs/automated.py | 6 +- src/finch/resources/jobs/manual.py | 5 +- src/finch/resources/payroll/pay_groups.py | 6 +- src/finch/resources/sandbox/employment.py | 6 +- src/finch/resources/sandbox/individual.py | 6 +- tests/test_utils/test_path.py | 89 ++++++++++++ 12 files changed, 254 insertions(+), 36 deletions(-) create mode 100644 src/finch/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/finch/_utils/__init__.py b/src/finch/_utils/__init__.py index dc64e29a..10cb66d2 100644 --- a/src/finch/_utils/__init__.py +++ b/src/finch/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/finch/_utils/_path.py b/src/finch/_utils/_path.py new file mode 100644 index 00000000..4d6e1e4c --- /dev/null +++ b/src/finch/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/finch/resources/hris/benefits/benefits.py b/src/finch/resources/hris/benefits/benefits.py index dd6d6e86..53f0265b 100644 --- a/src/finch/resources/hris/benefits/benefits.py +++ b/src/finch/resources/hris/benefits/benefits.py @@ -8,7 +8,7 @@ from .... import _legacy_response from ...._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform +from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from .individuals import ( Individuals, @@ -157,7 +157,7 @@ def retrieve( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._get( - f"/employer/benefits/{benefit_id}", + path_template("/employer/benefits/{benefit_id}", benefit_id=benefit_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -201,7 +201,7 @@ def update( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._post( - f"/employer/benefits/{benefit_id}", + path_template("/employer/benefits/{benefit_id}", benefit_id=benefit_id), body=maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams), options=make_request_options( extra_headers=extra_headers, @@ -414,7 +414,7 @@ async def retrieve( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return await self._get( - f"/employer/benefits/{benefit_id}", + path_template("/employer/benefits/{benefit_id}", benefit_id=benefit_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -460,7 +460,7 @@ async def update( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return await self._post( - f"/employer/benefits/{benefit_id}", + path_template("/employer/benefits/{benefit_id}", benefit_id=benefit_id), body=await async_maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams), options=make_request_options( extra_headers=extra_headers, diff --git a/src/finch/resources/hris/benefits/individuals.py b/src/finch/resources/hris/benefits/individuals.py index 0d9906de..f432915c 100644 --- a/src/finch/resources/hris/benefits/individuals.py +++ b/src/finch/resources/hris/benefits/individuals.py @@ -8,7 +8,7 @@ from .... import _legacy_response from ...._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ...._utils import maybe_transform, async_maybe_transform +from ...._utils import path_template, maybe_transform, async_maybe_transform from ...._compat import cached_property from ...._resource import SyncAPIResource, AsyncAPIResource from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -84,7 +84,7 @@ def enroll_many( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._post( - f"/employer/benefits/{benefit_id}/individuals", + path_template("/employer/benefits/{benefit_id}/individuals", benefit_id=benefit_id), body=maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), options=make_request_options( extra_headers=extra_headers, @@ -128,7 +128,7 @@ def enrolled_ids( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._get( - f"/employer/benefits/{benefit_id}/enrolled", + path_template("/employer/benefits/{benefit_id}/enrolled", benefit_id=benefit_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -175,7 +175,7 @@ def retrieve_many_benefits( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._get_api_list( - f"/employer/benefits/{benefit_id}/individuals", + path_template("/employer/benefits/{benefit_id}/individuals", benefit_id=benefit_id), page=SyncSinglePage[IndividualBenefit], options=make_request_options( extra_headers=extra_headers, @@ -226,7 +226,7 @@ def unenroll_many( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._delete( - f"/employer/benefits/{benefit_id}/individuals", + path_template("/employer/benefits/{benefit_id}/individuals", benefit_id=benefit_id), body=maybe_transform( {"individual_ids": individual_ids}, individual_unenroll_many_params.IndividualUnenrollManyParams ), @@ -300,7 +300,7 @@ async def enroll_many( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return await self._post( - f"/employer/benefits/{benefit_id}/individuals", + path_template("/employer/benefits/{benefit_id}/individuals", benefit_id=benefit_id), body=await async_maybe_transform(individuals, Iterable[individual_enroll_many_params.Individual]), options=make_request_options( extra_headers=extra_headers, @@ -344,7 +344,7 @@ async def enrolled_ids( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return await self._get( - f"/employer/benefits/{benefit_id}/enrolled", + path_template("/employer/benefits/{benefit_id}/enrolled", benefit_id=benefit_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -391,7 +391,7 @@ def retrieve_many_benefits( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return self._get_api_list( - f"/employer/benefits/{benefit_id}/individuals", + path_template("/employer/benefits/{benefit_id}/individuals", benefit_id=benefit_id), page=AsyncSinglePage[IndividualBenefit], options=make_request_options( extra_headers=extra_headers, @@ -442,7 +442,7 @@ async def unenroll_many( if not benefit_id: raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}") return await self._delete( - f"/employer/benefits/{benefit_id}/individuals", + path_template("/employer/benefits/{benefit_id}/individuals", benefit_id=benefit_id), body=await async_maybe_transform( {"individual_ids": individual_ids}, individual_unenroll_many_params.IndividualUnenrollManyParams ), diff --git a/src/finch/resources/hris/company/pay_statement_item/rules.py b/src/finch/resources/hris/company/pay_statement_item/rules.py index 55e44523..79b30ce8 100644 --- a/src/finch/resources/hris/company/pay_statement_item/rules.py +++ b/src/finch/resources/hris/company/pay_statement_item/rules.py @@ -9,7 +9,7 @@ from ..... import _legacy_response from ....._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ....._utils import maybe_transform, async_maybe_transform +from ....._utils import path_template, maybe_transform, async_maybe_transform from ....._compat import cached_property from ....._resource import SyncAPIResource, AsyncAPIResource from ....._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -143,7 +143,7 @@ def update( if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") return self._put( - f"/employer/pay-statement-item/rule/{rule_id}", + path_template("/employer/pay-statement-item/rule/{rule_id}", rule_id=rule_id), body=maybe_transform({"optional_property": optional_property}, rule_update_params.RuleUpdateParams), options=make_request_options( extra_headers=extra_headers, @@ -224,7 +224,7 @@ def delete( if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") return self._delete( - f"/employer/pay-statement-item/rule/{rule_id}", + path_template("/employer/pay-statement-item/rule/{rule_id}", rule_id=rule_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -351,7 +351,7 @@ async def update( if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") return await self._put( - f"/employer/pay-statement-item/rule/{rule_id}", + path_template("/employer/pay-statement-item/rule/{rule_id}", rule_id=rule_id), body=await async_maybe_transform( {"optional_property": optional_property}, rule_update_params.RuleUpdateParams ), @@ -434,7 +434,7 @@ async def delete( if not rule_id: raise ValueError(f"Expected a non-empty value for `rule_id` but received {rule_id!r}") return await self._delete( - f"/employer/pay-statement-item/rule/{rule_id}", + path_template("/employer/pay-statement-item/rule/{rule_id}", rule_id=rule_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/finch/resources/hris/documents.py b/src/finch/resources/hris/documents.py index ec32f30e..4ff2cd59 100644 --- a/src/finch/resources/hris/documents.py +++ b/src/finch/resources/hris/documents.py @@ -9,7 +9,7 @@ from ... import _legacy_response from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -137,7 +137,7 @@ def retreive( return cast( DocumentRetreiveResponse, self._get( - f"/employer/documents/{document_id}", + path_template("/employer/documents/{document_id}", document_id=document_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -269,7 +269,7 @@ async def retreive( return cast( DocumentRetreiveResponse, await self._get( - f"/employer/documents/{document_id}", + path_template("/employer/documents/{document_id}", document_id=document_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/finch/resources/jobs/automated.py b/src/finch/resources/jobs/automated.py index 161c6335..1b504e4a 100644 --- a/src/finch/resources/jobs/automated.py +++ b/src/finch/resources/jobs/automated.py @@ -8,7 +8,7 @@ from ... import _legacy_response from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import required_args, maybe_transform, async_maybe_transform +from ..._utils import path_template, required_args, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -182,7 +182,7 @@ def retrieve( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return self._get( - f"/jobs/automated/{job_id}", + path_template("/jobs/automated/{job_id}", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -405,7 +405,7 @@ async def retrieve( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return await self._get( - f"/jobs/automated/{job_id}", + path_template("/jobs/automated/{job_id}", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/finch/resources/jobs/manual.py b/src/finch/resources/jobs/manual.py index 596cda37..b622370d 100644 --- a/src/finch/resources/jobs/manual.py +++ b/src/finch/resources/jobs/manual.py @@ -6,6 +6,7 @@ from ... import _legacy_response from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -63,7 +64,7 @@ def retrieve( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return self._get( - f"/jobs/manual/{job_id}", + path_template("/jobs/manual/{job_id}", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -123,7 +124,7 @@ async def retrieve( if not job_id: raise ValueError(f"Expected a non-empty value for `job_id` but received {job_id!r}") return await self._get( - f"/jobs/manual/{job_id}", + path_template("/jobs/manual/{job_id}", job_id=job_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/finch/resources/payroll/pay_groups.py b/src/finch/resources/payroll/pay_groups.py index d4fe550f..b50c5245 100644 --- a/src/finch/resources/payroll/pay_groups.py +++ b/src/finch/resources/payroll/pay_groups.py @@ -6,7 +6,7 @@ from ... import _legacy_response from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -68,7 +68,7 @@ def retrieve( if not pay_group_id: raise ValueError(f"Expected a non-empty value for `pay_group_id` but received {pay_group_id!r}") return self._get( - f"/employer/pay-groups/{pay_group_id}", + path_template("/employer/pay-groups/{pay_group_id}", pay_group_id=pay_group_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -178,7 +178,7 @@ async def retrieve( if not pay_group_id: raise ValueError(f"Expected a non-empty value for `pay_group_id` but received {pay_group_id!r}") return await self._get( - f"/employer/pay-groups/{pay_group_id}", + path_template("/employer/pay-groups/{pay_group_id}", pay_group_id=pay_group_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/finch/resources/sandbox/employment.py b/src/finch/resources/sandbox/employment.py index 1db00690..e91ac89e 100644 --- a/src/finch/resources/sandbox/employment.py +++ b/src/finch/resources/sandbox/employment.py @@ -9,7 +9,7 @@ from ... import _legacy_response from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -127,7 +127,7 @@ def update( if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") return self._put( - f"/sandbox/employment/{individual_id}", + path_template("/sandbox/employment/{individual_id}", individual_id=individual_id), body=maybe_transform( { "class_code": class_code, @@ -268,7 +268,7 @@ async def update( if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") return await self._put( - f"/sandbox/employment/{individual_id}", + path_template("/sandbox/employment/{individual_id}", individual_id=individual_id), body=await async_maybe_transform( { "class_code": class_code, diff --git a/src/finch/resources/sandbox/individual.py b/src/finch/resources/sandbox/individual.py index a206dc7f..df7964cd 100644 --- a/src/finch/resources/sandbox/individual.py +++ b/src/finch/resources/sandbox/individual.py @@ -9,7 +9,7 @@ from ... import _legacy_response from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper @@ -114,7 +114,7 @@ def update( if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") return self._put( - f"/sandbox/individual/{individual_id}", + path_template("/sandbox/individual/{individual_id}", individual_id=individual_id), body=maybe_transform( { "dob": dob, @@ -236,7 +236,7 @@ async def update( if not individual_id: raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}") return await self._put( - f"/sandbox/individual/{individual_id}", + path_template("/sandbox/individual/{individual_id}", individual_id=individual_id), body=await async_maybe_transform( { "dob": dob, diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 00000000..42209a71 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from finch._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From 8ff1f27fe2442bfdc34937c1595fd23d2db9a60d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:03:42 +0000 Subject: [PATCH 23/53] refactor(tests): switch from prism to steady --- CONTRIBUTING.md | 2 +- scripts/mock | 26 +++++++++++++------------- scripts/test | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3eb5807c..ac662aaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ $ 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. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/scripts/mock b/scripts/mock index bcf3b392..3d1d19c1 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.3 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index dbeda2d2..d4fac354 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi From bfdc74c110317db9f811a74dffe77401972f2886 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:53:27 +0000 Subject: [PATCH 24/53] chore(tests): bump steady to v0.19.4 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 3d1d19c1..e2ca85a0 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.3 -- steady --version + npm exec --package=@stdy/cli@0.19.4 -- steady --version - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index d4fac354..4d15877e 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 2822044db38b48467ff6d82e060071833d2a4940 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:41:20 +0000 Subject: [PATCH 25/53] chore(tests): bump steady to v0.19.5 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index e2ca85a0..4f7dfd12 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.4 -- steady --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 4d15877e..3861edc6 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From fd09507b7fce652401ad9b70288ecd584810a91c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:41:03 +0000 Subject: [PATCH 26/53] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb189..3824f4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From b3457c13fc3dcbe72cfb4adba20f064785e54e1e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:01:50 +0000 Subject: [PATCH 27/53] chore(tests): bump steady to v0.19.6 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 4f7dfd12..dba30589 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.5 -- steady --version + npm exec --package=@stdy/cli@0.19.6 -- steady --version - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 3861edc6..004577df 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 2f97b57c54b536e47a0117d22ecbe1330532028b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:17:28 +0000 Subject: [PATCH 28/53] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb48c743..969fb914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/finch-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -38,7 +38,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From 634c5ef26def74ca00f796e01cdd1412e10e7f82 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:46:38 +0000 Subject: [PATCH 29/53] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index dba30589..9ecceca0 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 004577df..92315138 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From f5a025aec166054a52ee563f28352993682c6697 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:30:22 +0000 Subject: [PATCH 30/53] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index db08ac03..5e00d2b1 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-51b4b982d0bd629dbfe3e78a4a41f52154506a7a259f815f29e3ad402704b426.yml -openapi_spec_hash: 800e70018b89117b79b4c6628f6773f2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-0926c5a3f3e790c6a972e1a7b851e436419a727bc8c541d7a270af78ee65eab3.yml +openapi_spec_hash: bb1f4769845319413351c5bd9eb2d064 config_hash: d21a244fc073152c8dbecb8ece970209 From 9736c482b0e58023b806d55096a7d271395efdbb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:31:19 +0000 Subject: [PATCH 31/53] feat(api): api update --- .stats.yml | 4 ++-- src/finch/types/introspection.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5e00d2b1..3855d3fa 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-0926c5a3f3e790c6a972e1a7b851e436419a727bc8c541d7a270af78ee65eab3.yml -openapi_spec_hash: bb1f4769845319413351c5bd9eb2d064 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-94cd7551c2a8e8b2e491c9e8ca44387f2d1c7a498046dd60150f254316b20090.yml +openapi_spec_hash: c6f63a7d16d9ff16040c16a9633049fd config_hash: d21a244fc073152c8dbecb8ece970209 diff --git a/src/finch/types/introspection.py b/src/finch/types/introspection.py index 54c5417b..32783c15 100644 --- a/src/finch/types/introspection.py +++ b/src/finch/types/introspection.py @@ -54,6 +54,11 @@ class Entity(BaseModel): source_id: Optional[str] = None """The source ID of the entity""" + status: Literal[ + "pending", "processing", "connected", "error_no_account_setup", "error_permissions", "reauth", "disconnected" + ] + """The status of the entity connection""" + class Introspection(BaseModel): id: str From a8f9575597da48d407feac3c4d13d592b4241192 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:27:42 +0000 Subject: [PATCH 32/53] feat(internal): implement indices array format for query and form serialization --- scripts/mock | 4 ++-- scripts/test | 2 +- src/finch/_qs.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 9ecceca0..4931f304 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 92315138..52bcc4ce 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=brackets --validator-query-array-format=brackets --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 diff --git a/src/finch/_qs.py b/src/finch/_qs.py index ada6fd3f..de8c99bc 100644 --- a/src/finch/_qs.py +++ b/src/finch/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From bfd4d9c6047bcc277aa5629a200818998fc71026 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:47:27 +0000 Subject: [PATCH 33/53] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3855d3fa..91f464e0 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-94cd7551c2a8e8b2e491c9e8ca44387f2d1c7a498046dd60150f254316b20090.yml -openapi_spec_hash: c6f63a7d16d9ff16040c16a9633049fd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-63947213d9359808abc05e4c3cb53389325ca23c58d06bf293626f7d5d4fc2b8.yml +openapi_spec_hash: 50e4669590de9a411915a612615017d0 config_hash: d21a244fc073152c8dbecb8ece970209 From f92438d4a64c8b830068bed2ee9b6ec45a3d9562 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:27:47 +0000 Subject: [PATCH 34/53] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 4931f304..8b82c3e5 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 52bcc4ce..ed64d320 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 2abc18d84f6101550cf5d898cf5d5492f00e08fa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:21:25 +0000 Subject: [PATCH 35/53] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 8b82c3e5..886f2ffc 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index ed64d320..57cabda6 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From e9413d1d2f5684edd21808cb199e7b4cae9375f6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:11:29 +0000 Subject: [PATCH 36/53] fix(client): preserve hardcoded query params when merging with user params --- src/finch/_base_client.py | 4 ++++ tests/test_client.py | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/finch/_base_client.py b/src/finch/_base_client.py index 455e3ae1..cc335c1e 100644 --- a/src/finch/_base_client.py +++ b/src/finch/_base_client.py @@ -559,6 +559,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index 144a4cfd..611c6d40 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -501,6 +501,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Finch) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Finch) -> None: request = client._build_request( FinalRequestOptions( @@ -1540,6 +1564,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncFinch) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Finch) -> None: request = client._build_request( FinalRequestOptions( From 028a8051485aa5ef15c6f894ac20f11ba588211c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:56:36 +0000 Subject: [PATCH 37/53] fix: ensure file data are only sent as 1 parameter --- src/finch/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/finch/_utils/_utils.py b/src/finch/_utils/_utils.py index eec7f4a1..63b8cd60 100644 --- a/src/finch/_utils/_utils.py +++ b/src/finch/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 912c0a1b..8c71481a 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From 0a947a818fb4b1ccfe17cf7ee7a21b6d3c9d1bcc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:12:51 +0000 Subject: [PATCH 38/53] feat(api): api update --- .stats.yml | 2 +- src/finch/resources/connect/sessions.py | 16 ++++++++++++---- src/finch/types/connect/session_new_params.py | 6 +++++- .../connect/session_reauthenticate_params.py | 6 +++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.stats.yml b/.stats.yml index 91f464e0..957ae7a5 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-63947213d9359808abc05e4c3cb53389325ca23c58d06bf293626f7d5d4fc2b8.yml -openapi_spec_hash: 50e4669590de9a411915a612615017d0 +openapi_spec_hash: fe60fcf302d80a604a49ec1bf5068881 config_hash: d21a244fc073152c8dbecb8ece970209 diff --git a/src/finch/resources/connect/sessions.py b/src/finch/resources/connect/sessions.py index a89f5b15..99d3b2af 100644 --- a/src/finch/resources/connect/sessions.py +++ b/src/finch/resources/connect/sessions.py @@ -81,7 +81,9 @@ def new( customer_name: Name of the customer - products: The Finch products to request access to + products: The Finch products to request access to. Use `benefits` to access deductions + endpoints — `deduction` is a deprecated alias that is still accepted but should + not be combined with `benefits`. customer_email: Email address of the customer @@ -169,7 +171,9 @@ def reauthenticate( minutes_to_expire: The number of minutes until the session expires (defaults to 43,200, which is 30 days) - products: The products to request access to (optional for reauthentication) + products: The products to request access to (optional for reauthentication). Use + `benefits` to access deductions endpoints — `deduction` is a deprecated alias + that is still accepted but should not be combined with `benefits`. redirect_uri: The URI to redirect to after the Connect flow is completed @@ -263,7 +267,9 @@ async def new( customer_name: Name of the customer - products: The Finch products to request access to + products: The Finch products to request access to. Use `benefits` to access deductions + endpoints — `deduction` is a deprecated alias that is still accepted but should + not be combined with `benefits`. customer_email: Email address of the customer @@ -351,7 +357,9 @@ async def reauthenticate( minutes_to_expire: The number of minutes until the session expires (defaults to 43,200, which is 30 days) - products: The products to request access to (optional for reauthentication) + products: The products to request access to (optional for reauthentication). Use + `benefits` to access deductions endpoints — `deduction` is a deprecated alias + that is still accepted but should not be combined with `benefits`. redirect_uri: The URI to redirect to after the Connect flow is completed diff --git a/src/finch/types/connect/session_new_params.py b/src/finch/types/connect/session_new_params.py index 6822851b..a9777d03 100644 --- a/src/finch/types/connect/session_new_params.py +++ b/src/finch/types/connect/session_new_params.py @@ -31,7 +31,11 @@ class SessionNewParams(TypedDict, total=False): ] ] ] - """The Finch products to request access to""" + """The Finch products to request access to. + + Use `benefits` to access deductions endpoints — `deduction` is a deprecated + alias that is still accepted but should not be combined with `benefits`. + """ customer_email: Optional[str] """Email address of the customer""" diff --git a/src/finch/types/connect/session_reauthenticate_params.py b/src/finch/types/connect/session_reauthenticate_params.py index 291bb69f..d7e8bccb 100644 --- a/src/finch/types/connect/session_reauthenticate_params.py +++ b/src/finch/types/connect/session_reauthenticate_params.py @@ -34,7 +34,11 @@ class SessionReauthenticateParams(TypedDict, total=False): ] ] ] - """The products to request access to (optional for reauthentication)""" + """The products to request access to (optional for reauthentication). + + Use `benefits` to access deductions endpoints — `deduction` is a deprecated + alias that is still accepted but should not be combined with `benefits`. + """ redirect_uri: Optional[str] """The URI to redirect to after the Connect flow is completed""" From 2db68428534c562d825963c2d6d050c98ed4ec3b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:57:04 +0000 Subject: [PATCH 39/53] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 957ae7a5..5b71b33a 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-63947213d9359808abc05e4c3cb53389325ca23c58d06bf293626f7d5d4fc2b8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-a1778dd040aadddb53b90df5959043d17a5c9899f82c9c7188cd0be810426dc3.yml openapi_spec_hash: fe60fcf302d80a604a49ec1bf5068881 config_hash: d21a244fc073152c8dbecb8ece970209 From c4bd9ea002acfc7aec3f1413d97f71040000e0c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:14:35 +0000 Subject: [PATCH 40/53] feat(api): api update --- .stats.yml | 4 ++-- src/finch/types/job_completion_event.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5b71b33a..f9692078 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-a1778dd040aadddb53b90df5959043d17a5c9899f82c9c7188cd0be810426dc3.yml -openapi_spec_hash: fe60fcf302d80a604a49ec1bf5068881 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-b7c6baf733d255185a9767935a4eaa54f36678b641606cfb74a3050b67c59e9b.yml +openapi_spec_hash: e36cb22b7a3340ee040f8479636bb878 config_hash: d21a244fc073152c8dbecb8ece970209 diff --git a/src/finch/types/job_completion_event.py b/src/finch/types/job_completion_event.py index 331921f6..503e7357 100644 --- a/src/finch/types/job_completion_event.py +++ b/src/finch/types/job_completion_event.py @@ -28,5 +28,8 @@ class JobCompletionEvent(BaseWebhookEvent): "job.benefit_unenroll.completed", "job.benefit_update.completed", "job.data_sync_all.completed", + "job.w4_form_employee_sync.completed", + "job.initial_data_sync_org.succeeded", + "job.initial_data_sync_payroll.succeeded", ] ] = None From 4c5544ca2e32e65798b720cd89f9f3a3b5c8cbc7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:37:24 +0000 Subject: [PATCH 41/53] perf(client): optimize file structure copying in multipart requests --- src/finch/_files.py | 56 ++++++++++++++++++-- src/finch/_utils/__init__.py | 1 - src/finch/_utils/_utils.py | 15 ------ tests/test_deepcopy.py | 58 --------------------- tests/test_files.py | 99 +++++++++++++++++++++++++++++++++++- 5 files changed, 151 insertions(+), 78 deletions(-) delete mode 100644 tests/test_deepcopy.py diff --git a/src/finch/_files.py b/src/finch/_files.py index cc14c14f..0fdce17b 100644 --- a/src/finch/_files.py +++ b/src/finch/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/finch/_utils/__init__.py b/src/finch/_utils/__init__.py index 10cb66d2..1c090e51 100644 --- a/src/finch/_utils/__init__.py +++ b/src/finch/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/finch/_utils/_utils.py b/src/finch/_utils/_utils.py index 63b8cd60..771859f5 100644 --- a/src/finch/_utils/_utils.py +++ b/src/finch/_utils/_utils.py @@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 5fb88a94..00000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from finch._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_files.py b/tests/test_files.py index 0ed7d371..3310df8a 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from finch._files import to_httpx_files, async_to_httpx_files +from finch._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from finch._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } From 78e14212421f1cb9ad536fb94b4f3992800d5cee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:53:42 +0000 Subject: [PATCH 42/53] chore(tests): bump steady to v0.22.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 886f2ffc..04d29019 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.2 -- steady --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 57cabda6..7b05e44f 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 524ba9ee8dacdeb0cb810b409d9de754df330202 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:53:46 +0000 Subject: [PATCH 43/53] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee3..fe8451e4 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From 0608eea99892801e0575da4731a5717a3360be4b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:51:03 +0000 Subject: [PATCH 44/53] feat(api): api update --- .stats.yml | 4 ++-- src/finch/types/base_webhook_event.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f9692078..9d95cc59 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-b7c6baf733d255185a9767935a4eaa54f36678b641606cfb74a3050b67c59e9b.yml -openapi_spec_hash: e36cb22b7a3340ee040f8479636bb878 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch%2Ffinch-929f30a5a8ed80d2d5def22f9f52464b578a8924db0c2e37bb31f836181aa679.yml +openapi_spec_hash: 7559424a86d6c7768cee9c650d0c156c config_hash: d21a244fc073152c8dbecb8ece970209 diff --git a/src/finch/types/base_webhook_event.py b/src/finch/types/base_webhook_event.py index 2d7ab337..50c7cd84 100644 --- a/src/finch/types/base_webhook_event.py +++ b/src/finch/types/base_webhook_event.py @@ -24,3 +24,6 @@ class BaseWebhookEvent(BaseModel): connection_id: Optional[str] = None """Unique Finch ID of the connection associated with the webhook event.""" + + entity_id: Optional[str] = None + """Unique Finch id of the entity for which data has been updated.""" From f952a4d765d4295cbc6474787fa67b9dfa1fd19f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:02:00 +0000 Subject: [PATCH 45/53] fix: use correct field name format for multipart file arrays --- src/finch/_qs.py | 8 ++----- src/finch/_types.py | 3 +++ src/finch/_utils/_utils.py | 42 ++++++++++++++++++++++++++++++------- tests/test_extract_files.py | 28 ++++++++++++++++++++----- tests/test_files.py | 2 +- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/finch/_qs.py b/src/finch/_qs.py index de8c99bc..4127c19c 100644 --- a/src/finch/_qs.py +++ b/src/finch/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/finch/_types.py b/src/finch/_types.py index 705f9a00..677f982d 100644 --- a/src/finch/_types.py +++ b/src/finch/_types.py @@ -48,6 +48,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/finch/_utils/_utils.py b/src/finch/_utils/_utils.py index 771859f5..199cd231 100644 --- a/src/finch/_utils/_utils.py +++ b/src/finch/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 8c71481a..f05539b7 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from finch._types import FileTypes +from finch._types import FileTypes, ArrayFormat from finch._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index 3310df8a..2f991eed 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1}, From e3efceee56e129cc009ba0124341b49ad808f283 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:30:56 +0000 Subject: [PATCH 46/53] feat: support setting headers via env --- src/finch/_client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/finch/_client.py b/src/finch/_client.py index 4a2f3696..02e7c9c0 100644 --- a/src/finch/_client.py +++ b/src/finch/_client.py @@ -21,7 +21,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._models import SecurityOptions from ._version import __version__ @@ -118,6 +122,15 @@ def __init__( if base_url is None: base_url = f"https://api.tryfinch.com" + custom_headers_env = os.environ.get("FINCH_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -460,6 +473,15 @@ def __init__( if base_url is None: base_url = f"https://api.tryfinch.com" + custom_headers_env = os.environ.get("FINCH_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, From 0754983bc08c13464a4ebd601e2de28b8f43f7d3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:17:18 +0000 Subject: [PATCH 47/53] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 9d95cc59..f81ff4c2 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-929f30a5a8ed80d2d5def22f9f52464b578a8924db0c2e37bb31f836181aa679.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch/finch-929f30a5a8ed80d2d5def22f9f52464b578a8924db0c2e37bb31f836181aa679.yml openapi_spec_hash: 7559424a86d6c7768cee9c650d0c156c config_hash: d21a244fc073152c8dbecb8ece970209 From b252784d742519dcb558277260c41fdd30f2cf53 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:24:21 +0000 Subject: [PATCH 48/53] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f81ff4c2..07ec6c10 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/finch-929f30a5a8ed80d2d5def22f9f52464b578a8924db0c2e37bb31f836181aa679.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch/finch-326626d329d2a34acc127d7afe18edb068b1c28a8b50fecb4bb9973d3f9337a4.yml openapi_spec_hash: 7559424a86d6c7768cee9c650d0c156c config_hash: d21a244fc073152c8dbecb8ece970209 From 5fa44b46e84b4fb563fb972653917703f3a5f0e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:44:44 +0000 Subject: [PATCH 49/53] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 07ec6c10..09d5b6a9 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/finch-326626d329d2a34acc127d7afe18edb068b1c28a8b50fecb4bb9973d3f9337a4.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch/finch-9df6cda4c8e1134b03b91173b0ba39eb902d8a2d7db5197c2a54b2edafb6b40b.yml openapi_spec_hash: 7559424a86d6c7768cee9c650d0c156c -config_hash: d21a244fc073152c8dbecb8ece970209 +config_hash: 429708b67ee9e80003db82611677296c From d713cd0cebe21fbeea5e46f075c3bd7eb9f4fbe0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:59:30 +0000 Subject: [PATCH 50/53] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09b77674..f2135b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/finch/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/finch/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From abb9fa0f5555143e8dfcd483e859d4a2a6d5c9c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 16:40:33 +0000 Subject: [PATCH 51/53] feat(api): api update --- .stats.yml | 4 +- .../types/hris/benefits/individual_benefit.py | 54 ++++---- src/finch/types/hris/employment_data.py | 126 +----------------- src/finch/types/hris/individual.py | 78 +---------- 4 files changed, 38 insertions(+), 224 deletions(-) diff --git a/.stats.yml b/.stats.yml index 09d5b6a9..0681ca46 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/finch-9df6cda4c8e1134b03b91173b0ba39eb902d8a2d7db5197c2a54b2edafb6b40b.yml -openapi_spec_hash: 7559424a86d6c7768cee9c650d0c156c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/finch/finch-c8a0d5eca390ea3ab6e8b5b7d9b46d9a22e34d81aeab444c8ce1b5a94eba0028.yml +openapi_spec_hash: e261a3289242d3ad52542f1491a903ee config_hash: 429708b67ee9e80003db82611677296c diff --git a/src/finch/types/hris/benefits/individual_benefit.py b/src/finch/types/hris/benefits/individual_benefit.py index 14278f19..fab0461d 100644 --- a/src/finch/types/hris/benefits/individual_benefit.py +++ b/src/finch/types/hris/benefits/individual_benefit.py @@ -8,20 +8,20 @@ __all__ = [ "IndividualBenefit", "Body", - "BodyUnionMember0", - "BodyUnionMember0CompanyContribution", - "BodyUnionMember0CompanyContributionUnionMember0", - "BodyUnionMember0CompanyContributionUnionMember1", - "BodyUnionMember0CompanyContributionUnionMember2", - "BodyUnionMember0CompanyContributionUnionMember2Tier", - "BodyUnionMember0EmployeeDeduction", - "BodyUnionMember0EmployeeDeductionUnionMember0", - "BodyUnionMember0EmployeeDeductionUnionMember1", + "BodyIndividualBenefit", + "BodyIndividualBenefitCompanyContribution", + "BodyIndividualBenefitCompanyContributionCompanyContributionFixed", + "BodyIndividualBenefitCompanyContributionCompanyContributionPercent", + "BodyIndividualBenefitCompanyContributionCompanyContributionTiered", + "BodyIndividualBenefitCompanyContributionCompanyContributionTieredTier", + "BodyIndividualBenefitEmployeeDeduction", + "BodyIndividualBenefitEmployeeDeductionEmployeeDeductionContributionFixed", + "BodyIndividualBenefitEmployeeDeductionEmployeeDeductionContributionPercent", "BodyBatchError", ] -class BodyUnionMember0CompanyContributionUnionMember0(BaseModel): +class BodyIndividualBenefitCompanyContributionCompanyContributionFixed(BaseModel): amount: int """ Contribution amount in cents (for type=fixed) or basis points (for type=percent, @@ -36,7 +36,7 @@ class BodyUnionMember0CompanyContributionUnionMember0(BaseModel): """ -class BodyUnionMember0CompanyContributionUnionMember1(BaseModel): +class BodyIndividualBenefitCompanyContributionCompanyContributionPercent(BaseModel): amount: int """ Contribution amount in cents (for type=fixed) or basis points (for type=percent, @@ -51,14 +51,14 @@ class BodyUnionMember0CompanyContributionUnionMember1(BaseModel): """ -class BodyUnionMember0CompanyContributionUnionMember2Tier(BaseModel): +class BodyIndividualBenefitCompanyContributionCompanyContributionTieredTier(BaseModel): match: int threshold: int -class BodyUnionMember0CompanyContributionUnionMember2(BaseModel): - tiers: List[BodyUnionMember0CompanyContributionUnionMember2Tier] +class BodyIndividualBenefitCompanyContributionCompanyContributionTiered(BaseModel): + tiers: List[BodyIndividualBenefitCompanyContributionCompanyContributionTieredTier] """ Array of tier objects defining employer match tiers based on employee contribution thresholds. Required when type=tiered. @@ -72,15 +72,15 @@ class BodyUnionMember0CompanyContributionUnionMember2(BaseModel): """ -BodyUnionMember0CompanyContribution: TypeAlias = Union[ - BodyUnionMember0CompanyContributionUnionMember0, - BodyUnionMember0CompanyContributionUnionMember1, - BodyUnionMember0CompanyContributionUnionMember2, +BodyIndividualBenefitCompanyContribution: TypeAlias = Union[ + BodyIndividualBenefitCompanyContributionCompanyContributionFixed, + BodyIndividualBenefitCompanyContributionCompanyContributionPercent, + BodyIndividualBenefitCompanyContributionCompanyContributionTiered, None, ] -class BodyUnionMember0EmployeeDeductionUnionMember0(BaseModel): +class BodyIndividualBenefitEmployeeDeductionEmployeeDeductionContributionFixed(BaseModel): amount: int """ Contribution amount in cents (for type=fixed) or basis points (for type=percent, @@ -95,7 +95,7 @@ class BodyUnionMember0EmployeeDeductionUnionMember0(BaseModel): """ -class BodyUnionMember0EmployeeDeductionUnionMember1(BaseModel): +class BodyIndividualBenefitEmployeeDeductionEmployeeDeductionContributionPercent(BaseModel): amount: int """ Contribution amount in cents (for type=fixed) or basis points (for type=percent, @@ -110,12 +110,14 @@ class BodyUnionMember0EmployeeDeductionUnionMember1(BaseModel): """ -BodyUnionMember0EmployeeDeduction: TypeAlias = Union[ - BodyUnionMember0EmployeeDeductionUnionMember0, BodyUnionMember0EmployeeDeductionUnionMember1, None +BodyIndividualBenefitEmployeeDeduction: TypeAlias = Union[ + BodyIndividualBenefitEmployeeDeductionEmployeeDeductionContributionFixed, + BodyIndividualBenefitEmployeeDeductionEmployeeDeductionContributionPercent, + None, ] -class BodyUnionMember0(BaseModel): +class BodyIndividualBenefit(BaseModel): annual_maximum: Optional[int] = None """ If the benefit supports annual maximum, the amount in cents for this individual. @@ -127,14 +129,14 @@ class BodyUnionMember0(BaseModel): for this individual. """ - company_contribution: Optional[BodyUnionMember0CompanyContribution] = None + company_contribution: Optional[BodyIndividualBenefitCompanyContribution] = None """Company contribution configuration. Supports fixed amounts (in cents), percentage-based contributions (in basis points where 100 = 1%), or tiered matching structures. """ - employee_deduction: Optional[BodyUnionMember0EmployeeDeduction] = None + employee_deduction: Optional[BodyIndividualBenefitEmployeeDeduction] = None """Employee deduction configuration. Supports both fixed amounts (in cents) and percentage-based contributions (in @@ -155,7 +157,7 @@ class BodyBatchError(BaseModel): finch_code: Optional[str] = None -Body: TypeAlias = Union[BodyUnionMember0, BodyBatchError] +Body: TypeAlias = Union[BodyIndividualBenefit, BodyBatchError] class IndividualBenefit(BaseModel): diff --git a/src/finch/types/hris/employment_data.py b/src/finch/types/hris/employment_data.py index 2a9409d6..3460cfb0 100644 --- a/src/finch/types/hris/employment_data.py +++ b/src/finch/types/hris/employment_data.py @@ -1,129 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Union, Optional -from typing_extensions import Literal, TypeAlias +from typing import Union, Optional +from typing_extensions import TypeAlias -from ..income import Income from ..._models import BaseModel -from ..location import Location -__all__ = [ - "EmploymentData", - "UnionMember0", - "UnionMember0Department", - "UnionMember0Employment", - "UnionMember0Manager", - "UnionMember0CustomField", - "BatchError", -] - - -class UnionMember0Department(BaseModel): - """The department object.""" - - name: Optional[str] = None - """The name of the department associated with the individual.""" - - -class UnionMember0Employment(BaseModel): - """The employment object.""" - - subtype: Optional[Literal["full_time", "intern", "part_time", "temp", "seasonal", "individual_contractor"]] = None - """The secondary employment type of the individual. - - Options: `full_time`, `part_time`, `intern`, `temp`, `seasonal` and - `individual_contractor`. - """ - - type: Optional[Literal["employee", "contractor"]] = None - """The main employment type of the individual.""" - - -class UnionMember0Manager(BaseModel): - """The manager object representing the manager of the individual within the org.""" - - id: str - """A stable Finch `id` (UUID v4) for an individual in the company.""" - - -class UnionMember0CustomField(BaseModel): - name: Optional[str] = None - - value: Union[Optional[str], Optional[List[object]], Optional[float], Optional[bool], Optional[object], None] = None - - -class UnionMember0(BaseModel): - id: str - """A stable Finch `id` (UUID v4) for an individual in the company.""" - - class_code: Optional[str] = None - """Worker's compensation classification code for this employee""" - - department: Optional[UnionMember0Department] = None - """The department object.""" - - employment: Optional[UnionMember0Employment] = None - """The employment object.""" - - employment_status: Optional[ - Literal["active", "deceased", "leave", "onboarding", "prehire", "retired", "terminated"] - ] = None - """The detailed employment status of the individual.""" - - end_date: Optional[str] = None - - first_name: Optional[str] = None - """The legal first name of the individual.""" - - flsa_status: Optional[Literal["exempt", "non_exempt", "unknown"]] = None - """The FLSA status of the individual. - - Available options: `exempt`, `non_exempt`, `unknown`. - """ - - is_active: Optional[bool] = None - """`true` if the individual an an active employee or contractor at the company.""" - - last_name: Optional[str] = None - """The legal last name of the individual.""" - - latest_rehire_date: Optional[str] = None - - location: Optional[Location] = None - - manager: Optional[UnionMember0Manager] = None - """The manager object representing the manager of the individual within the org.""" - - middle_name: Optional[str] = None - """The legal middle name of the individual.""" - - start_date: Optional[str] = None - - title: Optional[str] = None - """The current title of the individual.""" - - custom_fields: Optional[List[UnionMember0CustomField]] = None - """Custom fields for the individual. - - These are fields which are defined by the employer in the system. Custom fields - are not currently supported for assisted connections. - """ - - income_history: Optional[List[Optional[Income]]] = None - """The array of income history.""" - - income: Optional[Income] = None - """The employee's income as reported by the provider. - - This may not always be annualized income, but may be in units of bi-weekly, - semi-monthly, daily, etc, depending on what information the provider returns. - """ - - source_id: Optional[str] = None - """The source system's unique employment identifier for this individual""" - - work_id: Optional[str] = None - """This field is deprecated in favour of `source_id`""" +__all__ = ["EmploymentData", "BatchError"] class BatchError(BaseModel): @@ -136,4 +18,4 @@ class BatchError(BaseModel): finch_code: Optional[str] = None -EmploymentData: TypeAlias = Union[UnionMember0, BatchError] +EmploymentData: TypeAlias = Union[EmploymentData, BatchError] diff --git a/src/finch/types/hris/individual.py b/src/finch/types/hris/individual.py index 926f7c67..90444900 100644 --- a/src/finch/types/hris/individual.py +++ b/src/finch/types/hris/individual.py @@ -1,81 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Union, Optional -from typing_extensions import Literal, TypeAlias +from typing import Union, Optional +from typing_extensions import TypeAlias from ..._models import BaseModel -from ..location import Location -__all__ = ["Individual", "UnionMember0", "UnionMember0PhoneNumber", "UnionMember0Email", "BatchError"] - - -class UnionMember0PhoneNumber(BaseModel): - data: Optional[str] = None - - type: Optional[Literal["work", "personal"]] = None - - -class UnionMember0Email(BaseModel): - data: str - - type: Optional[Literal["work", "personal"]] = None - - -class UnionMember0(BaseModel): - id: str - """A stable Finch `id` (UUID v4) for an individual in the company.""" - - dob: Optional[str] = None - - ethnicity: Optional[ - Literal[ - "asian", - "white", - "black_or_african_american", - "native_hawaiian_or_pacific_islander", - "american_indian_or_alaska_native", - "hispanic_or_latino", - "two_or_more_races", - "decline_to_specify", - ] - ] = None - """The EEOC-defined ethnicity of the individual.""" - - first_name: Optional[str] = None - """The legal first name of the individual.""" - - gender: Optional[Literal["female", "male", "other", "decline_to_specify"]] = None - """The gender of the individual.""" - - last_name: Optional[str] = None - """The legal last name of the individual.""" - - middle_name: Optional[str] = None - """The legal middle name of the individual.""" - - phone_numbers: Optional[List[Optional[UnionMember0PhoneNumber]]] = None - - preferred_name: Optional[str] = None - """The preferred name of the individual.""" - - residence: Optional[Location] = None - - emails: Optional[List[UnionMember0Email]] = None - - encrypted_ssn: Optional[str] = None - """Social Security Number of the individual in **encrypted** format. - - This field is only available with the `ssn` scope enabled and the - `options: { include: ['ssn'] }` param set in the body. - """ - - ssn: Optional[str] = None - """Social Security Number of the individual. - - This field is only available with the `ssn` scope enabled and the - `options: { include: ['ssn'] }` param set in the body. - [Click here to learn more about enabling the SSN field](/developer-resources/Enable-SSN-Field). - """ +__all__ = ["Individual", "BatchError"] class BatchError(BaseModel): @@ -88,4 +18,4 @@ class BatchError(BaseModel): finch_code: Optional[str] = None -Individual: TypeAlias = Union[UnionMember0, BatchError] +Individual: TypeAlias = Union[Individual, BatchError] From 99e028ee64f045980b7245948adc86b02ffeb63f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 16:26:01 +0000 Subject: [PATCH 52/53] fix(client): add missing f-string prefix in file type error message --- src/finch/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finch/_files.py b/src/finch/_files.py index 0fdce17b..76da9e08 100644 --- a/src/finch/_files.py +++ b/src/finch/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From 953ee69369b4246fcfadb93956b0466babf8c489 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 16:26:51 +0000 Subject: [PATCH 53/53] release: 1.45.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 68 +++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/finch/_version.py | 2 +- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1ee5dee6..6d2723c7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.44.1" + ".": "1.45.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 809425a0..9ffc6075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,73 @@ # Changelog +## 1.45.0 (2026-05-08) + +Full Changelog: [v1.44.1...v1.45.0](https://github.com/Finch-API/finch-api-python/compare/v1.44.1...v1.45.0) + +### Features + +* **api:** add per endpoint security ([686b27e](https://github.com/Finch-API/finch-api-python/commit/686b27e6d19cc5858b420e2b1bb265f50791c7ea)) +* **api:** api update ([abb9fa0](https://github.com/Finch-API/finch-api-python/commit/abb9fa0f5555143e8dfcd483e859d4a2a6d5c9c0)) +* **api:** api update ([0608eea](https://github.com/Finch-API/finch-api-python/commit/0608eea99892801e0575da4731a5717a3360be4b)) +* **api:** api update ([c4bd9ea](https://github.com/Finch-API/finch-api-python/commit/c4bd9ea002acfc7aec3f1413d97f71040000e0c9)) +* **api:** api update ([0a947a8](https://github.com/Finch-API/finch-api-python/commit/0a947a818fb4b1ccfe17cf7ee7a21b6d3c9d1bcc)) +* **api:** api update ([9736c48](https://github.com/Finch-API/finch-api-python/commit/9736c482b0e58023b806d55096a7d271395efdbb)) +* **api:** api update ([c6631fc](https://github.com/Finch-API/finch-api-python/commit/c6631fc8d74f89931be556746c6ce67e4cb62867)) +* **client:** add custom JSON encoder for extended type support ([02cb5a0](https://github.com/Finch-API/finch-api-python/commit/02cb5a0bdcdbd8b4652937928ca7d0b336f6c53c)) +* **internal:** implement indices array format for query and form serialization ([a8f9575](https://github.com/Finch-API/finch-api-python/commit/a8f9575597da48d407feac3c4d13d592b4241192)) +* support setting headers via env ([e3efcee](https://github.com/Finch-API/finch-api-python/commit/e3efceee56e129cc009ba0124341b49ad808f283)) + + +### Bug Fixes + +* **api:** remove invalid transform config ([565de84](https://github.com/Finch-API/finch-api-python/commit/565de8480f61bf07d5dbd0fb39fbb8b4f5ea9671)) +* **client:** add missing f-string prefix in file type error message ([99e028e](https://github.com/Finch-API/finch-api-python/commit/99e028ee64f045980b7245948adc86b02ffeb63f)) +* **client:** preserve hardcoded query params when merging with user params ([e9413d1](https://github.com/Finch-API/finch-api-python/commit/e9413d1d2f5684edd21808cb199e7b4cae9375f6)) +* **deps:** bump minimum typing-extensions version ([16af387](https://github.com/Finch-API/finch-api-python/commit/16af387fd95f0207db3ce4a072cab0946ce977af)) +* **docs:** fix mcp installation instructions for remote servers ([298ddec](https://github.com/Finch-API/finch-api-python/commit/298ddec8fded9a72146800253d644cb96227cbde)) +* ensure file data are only sent as 1 parameter ([028a805](https://github.com/Finch-API/finch-api-python/commit/028a8051485aa5ef15c6f894ac20f11ba588211c)) +* **pydantic:** do not pass `by_alias` unless set ([399bc94](https://github.com/Finch-API/finch-api-python/commit/399bc94cf48e0518f87b239ead5e869d66b03fce)) +* sanitize endpoint path params ([896338f](https://github.com/Finch-API/finch-api-python/commit/896338f100cb035ad2ff29d9c7f199836aafe85c)) +* **tests:** skip broken date validation test ([e22eb0f](https://github.com/Finch-API/finch-api-python/commit/e22eb0ffed3e42143600c931f2ab5b342160230c)) +* use correct field name format for multipart file arrays ([f952a4d](https://github.com/Finch-API/finch-api-python/commit/f952a4d765d4295cbc6474787fa67b9dfa1fd19f)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([4c5544c](https://github.com/Finch-API/finch-api-python/commit/4c5544ca2e32e65798b720cd89f9f3a3b5c8cbc7)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([2f97b57](https://github.com/Finch-API/finch-api-python/commit/2f97b57c54b536e47a0117d22ecbe1330532028b)) +* **ci:** skip uploading artifacts on stainless-internal branches ([0d13335](https://github.com/Finch-API/finch-api-python/commit/0d13335d29a546c8cc94e67b59c19835821f974e)) +* **ci:** upgrade `actions/github-script` ([f342729](https://github.com/Finch-API/finch-api-python/commit/f3427295a99f7aa4a45412f699ec6b3cc41b16c2)) +* format all `api.md` files ([8555964](https://github.com/Finch-API/finch-api-python/commit/8555964275d168e400ef190422baee5a1983eeb1)) +* **internal:** add request options to SSE classes ([977dcfa](https://github.com/Finch-API/finch-api-python/commit/977dcfa5e7da78f0d4b465843e52a4fd2999799b)) +* **internal:** bump dependencies ([0b67a64](https://github.com/Finch-API/finch-api-python/commit/0b67a64b36f6824c1000d126e5ed707e495c3c59)) +* **internal:** codegen related update ([94aae5c](https://github.com/Finch-API/finch-api-python/commit/94aae5c36d464596cda9c899f64be67959fd9cb5)) +* **internal:** codegen related update ([b746d12](https://github.com/Finch-API/finch-api-python/commit/b746d12f98b1fcb287bf1b7844fc8a2d864a7d86)) +* **internal:** fix lint error on Python 3.14 ([b0f5ba3](https://github.com/Finch-API/finch-api-python/commit/b0f5ba3c117a989524c31e6c62ab9215f98fccd2)) +* **internal:** make `test_proxy_environment_variables` more resilient ([e33584f](https://github.com/Finch-API/finch-api-python/commit/e33584fc7bc4aaaa73377f8436f9858190266236)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([9f092c7](https://github.com/Finch-API/finch-api-python/commit/9f092c7d989fbe863c8d8624238694c564a871f0)) +* **internal:** more robust bootstrap script ([524ba9e](https://github.com/Finch-API/finch-api-python/commit/524ba9ee8dacdeb0cb810b409d9de754df330202)) +* **internal:** reformat pyproject.toml ([d713cd0](https://github.com/Finch-API/finch-api-python/commit/d713cd0cebe21fbeea5e46f075c3bd7eb9f4fbe0)) +* **internal:** tweak CI branches ([9e540f1](https://github.com/Finch-API/finch-api-python/commit/9e540f1c1e03cd4099b4b7338fc689c569bd8e54)) +* **internal:** update gitignore ([fd09507](https://github.com/Finch-API/finch-api-python/commit/fd09507b7fce652401ad9b70288ecd584810a91c)) +* **tests:** bump steady to v0.19.4 ([bfdc74c](https://github.com/Finch-API/finch-api-python/commit/bfdc74c110317db9f811a74dffe77401972f2886)) +* **tests:** bump steady to v0.19.5 ([2822044](https://github.com/Finch-API/finch-api-python/commit/2822044db38b48467ff6d82e060071833d2a4940)) +* **tests:** bump steady to v0.19.6 ([b3457c1](https://github.com/Finch-API/finch-api-python/commit/b3457c13fc3dcbe72cfb4adba20f064785e54e1e)) +* **tests:** bump steady to v0.19.7 ([634c5ef](https://github.com/Finch-API/finch-api-python/commit/634c5ef26def74ca00f796e01cdd1412e10e7f82)) +* **tests:** bump steady to v0.20.1 ([f92438d](https://github.com/Finch-API/finch-api-python/commit/f92438d4a64c8b830068bed2ee9b6ec45a3d9562)) +* **tests:** bump steady to v0.20.2 ([2abc18d](https://github.com/Finch-API/finch-api-python/commit/2abc18d84f6101550cf5d898cf5d5492f00e08fa)) +* **tests:** bump steady to v0.22.1 ([78e1421](https://github.com/Finch-API/finch-api-python/commit/78e14212421f1cb9ad536fb94b4f3992800d5cee)) +* update mock server docs ([7891a91](https://github.com/Finch-API/finch-api-python/commit/7891a91c042ae55b9aeaec5980b81197b8692039)) + + +### Refactors + +* **tests:** switch from prism to steady ([8ff1f27](https://github.com/Finch-API/finch-api-python/commit/8ff1f27fe2442bfdc34937c1595fd23d2db9a60d)) + ## 1.44.1 (2026-01-16) Full Changelog: [v1.44.0...v1.44.1](https://github.com/Finch-API/finch-api-python/compare/v1.44.0...v1.44.1) diff --git a/pyproject.toml b/pyproject.toml index f2135b36..9a880024 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "finch-api" -version = "1.44.1" +version = "1.45.0" description = "The official Python library for the Finch API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/finch/_version.py b/src/finch/_version.py index 337c1dbd..d8308dd6 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.44.1" # x-release-please-version +__version__ = "1.45.0" # x-release-please-version