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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 b1dd557f9590b3d2917e4f022c7c9b318962d90b 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:35:09 +0000 Subject: [PATCH 5/5] release: 1.45.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 19 +++++++++++++++++++ pyproject.toml | 2 +- src/finch/_version.py | 2 +- 4 files changed, 22 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..52cc0801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 1.45.0 (2026-01-26) + +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)) + + +### Bug Fixes + +* **tests:** skip broken date validation test ([e22eb0f](https://github.com/Finch-API/finch-api-python/commit/e22eb0ffed3e42143600c931f2ab5b342160230c)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([f342729](https://github.com/Finch-API/finch-api-python/commit/f3427295a99f7aa4a45412f699ec6b3cc41b16c2)) +* **internal:** codegen related update ([b746d12](https://github.com/Finch-API/finch-api-python/commit/b746d12f98b1fcb287bf1b7844fc8a2d864a7d86)) + ## 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 5907bdab..0215dfc1 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