Skip to content

Commit 7961a22

Browse files
committed
chore: revert temporary commit
1 parent 5e42474 commit 7961a22

File tree

8 files changed

+654
-7
lines changed

8 files changed

+654
-7
lines changed

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,29 @@ page = client.hris.directory.list()
142142
print(page.page)
143143
```
144144

145+
## Webhook Verification
146+
147+
We provide helper methods for verifying that a webhook request came from Finch, and not a malicious third party.
148+
149+
You can use `finch.webhooks.verify_signature(body: string, headers, secret?) -> None` or `finch.webhooks.unwrap(body: string, headers, secret?) -> Payload`,
150+
both of which will raise an error if the signature is invalid.
151+
152+
Note that the "body" parameter must be the raw JSON string sent from the server (do not parse it first).
153+
The `.unwrap()` method can parse this JSON for you into a `Payload` object.
154+
155+
For example, in [FastAPI](https://fastapi.tiangolo.com/):
156+
157+
```py
158+
@app.post('/my-webhook-handler')
159+
async def handler(request: Request):
160+
body = await request.body()
161+
secret = os.environ['FINCH_WEBHOOK_SECRET'] # env var used by default; explicit here.
162+
payload = client.webhooks.unwrap(body, request.headers, secret)
163+
print(payload)
164+
165+
return {'ok': True}
166+
```
167+
145168
## Handling errors
146169

147170
When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `finch.APIConnectionError` is raised.

api.md

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
from finch.types import ConnectionStatusType, OperationSupport, OperationSupportMatrix, Paging
55
```
66

7+
# Finch
8+
9+
Methods:
10+
11+
- <code>client.<a href="./src/finch/_client.py">get_auth_url</a>(\*args) -> str</code>
12+
- <code>client.<a href="./src/finch/_client.py">with_access_token</a>(\*args) -> Self</code>
13+
714
# AccessTokens
815

916
Types:
@@ -188,6 +195,11 @@ from finch.types import (
188195
)
189196
```
190197

198+
Methods:
199+
200+
- <code>client.webhooks.<a href="./src/finch/resources/webhooks.py">unwrap</a>(\*args) -> WebhookEvent</code>
201+
- <code>client.webhooks.<a href="./src/finch/resources/webhooks.py">verify_signature</a>(\*args) -> None</code>
202+
191203
# RequestForwarding
192204

193205
Types:

src/finch/_client.py

+132
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Finch(SyncAPIClient):
5656
hris: resources.HRIS
5757
providers: resources.Providers
5858
account: resources.Account
59+
webhooks: resources.Webhooks
5960
request_forwarding: resources.RequestForwarding
6061
jobs: resources.Jobs
6162
sandbox: resources.Sandbox
@@ -156,6 +157,7 @@ def __init__(
156157
self.hris = resources.HRIS(self)
157158
self.providers = resources.Providers(self)
158159
self.account = resources.Account(self)
160+
self.webhooks = resources.Webhooks(self)
159161
self.request_forwarding = resources.RequestForwarding(self)
160162
self.jobs = resources.Jobs(self)
161163
self.sandbox = resources.Sandbox(self)
@@ -299,6 +301,70 @@ def copy(
299301
# client.with_options(timeout=10).foo.create(...)
300302
with_options = copy
301303

304+
def get_access_token(
305+
self,
306+
code: str,
307+
*,
308+
redirect_uri: str | None = None,
309+
) -> str:
310+
"""DEPRECATED: use client.access_tokens.create instead."""
311+
if self.client_id is None:
312+
raise ValueError("Expected client_id to be set in order to call get_access_token")
313+
314+
if self.client_secret is None:
315+
raise ValueError("Expected client_secret to be set in order to call get_access_token")
316+
317+
response = self.post(
318+
"/auth/token",
319+
body={
320+
"client_id": self.client_id,
321+
"client_secret": self.client_secret,
322+
"code": code,
323+
"redirect_uri": redirect_uri,
324+
},
325+
options={"headers": {"Authorization": Omit()}},
326+
cast_to=httpx.Response,
327+
)
328+
data = response.json()
329+
return str(data["access_token"])
330+
331+
def get_auth_url(
332+
self,
333+
*,
334+
products: str,
335+
redirect_uri: str,
336+
sandbox: bool,
337+
) -> str:
338+
"""
339+
Returns the authorization url which can be visited in order to obtain an
340+
authorization code from Finch. The authorization code can then be exchanged for
341+
an access token for the Finch api by calling get_access_token().
342+
"""
343+
if self.client_id is None:
344+
raise ValueError("Expected the client_id to be set in order to call get_auth_url")
345+
346+
return str(
347+
httpx.URL(
348+
"https://connect.tryfinch.com/authorize",
349+
params={
350+
"client_id": self.client_id,
351+
"products": products,
352+
"redirect_uri": redirect_uri,
353+
"sandbox": sandbox,
354+
},
355+
)
356+
)
357+
358+
def with_access_token(
359+
self,
360+
access_token: str,
361+
) -> Self:
362+
"""
363+
Returns a copy of the current Finch client with the given access token for
364+
authentication.
365+
"""
366+
return self.with_options(access_token=access_token)
367+
302368
@override
303369
def _make_status_error(
304370
self,
@@ -338,6 +404,7 @@ class AsyncFinch(AsyncAPIClient):
338404
hris: resources.AsyncHRIS
339405
providers: resources.AsyncProviders
340406
account: resources.AsyncAccount
407+
webhooks: resources.AsyncWebhooks
341408
request_forwarding: resources.AsyncRequestForwarding
342409
jobs: resources.AsyncJobs
343410
sandbox: resources.AsyncSandbox
@@ -438,6 +505,7 @@ def __init__(
438505
self.hris = resources.AsyncHRIS(self)
439506
self.providers = resources.AsyncProviders(self)
440507
self.account = resources.AsyncAccount(self)
508+
self.webhooks = resources.AsyncWebhooks(self)
441509
self.request_forwarding = resources.AsyncRequestForwarding(self)
442510
self.jobs = resources.AsyncJobs(self)
443511
self.sandbox = resources.AsyncSandbox(self)
@@ -581,6 +649,70 @@ def copy(
581649
# client.with_options(timeout=10).foo.create(...)
582650
with_options = copy
583651

652+
async def get_access_token(
653+
self,
654+
code: str,
655+
*,
656+
redirect_uri: str | None = None,
657+
) -> str:
658+
"""DEPRECATED: use client.access_tokens.create instead."""
659+
if self.client_id is None:
660+
raise ValueError("Expected client_id to be set in order to call get_access_token")
661+
662+
if self.client_secret is None:
663+
raise ValueError("Expected client_secret to be set in order to call get_access_token")
664+
665+
response = await self.post(
666+
"/auth/token",
667+
body={
668+
"client_id": self.client_id,
669+
"client_secret": self.client_secret,
670+
"code": code,
671+
"redirect_uri": redirect_uri,
672+
},
673+
options={"headers": {"Authorization": Omit()}},
674+
cast_to=httpx.Response,
675+
)
676+
data = response.json()
677+
return str(data["access_token"])
678+
679+
def get_auth_url(
680+
self,
681+
*,
682+
products: str,
683+
redirect_uri: str,
684+
sandbox: bool,
685+
) -> str:
686+
"""
687+
Returns the authorization url which can be visited in order to obtain an
688+
authorization code from Finch. The authorization code can then be exchanged for
689+
an access token for the Finch api by calling get_access_token().
690+
"""
691+
if self.client_id is None:
692+
raise ValueError("Expected the client_id to be set in order to call get_auth_url")
693+
694+
return str(
695+
httpx.URL(
696+
"https://connect.tryfinch.com/authorize",
697+
params={
698+
"client_id": self.client_id,
699+
"products": products,
700+
"redirect_uri": redirect_uri,
701+
"sandbox": sandbox,
702+
},
703+
)
704+
)
705+
706+
def with_access_token(
707+
self,
708+
access_token: str,
709+
) -> Self:
710+
"""
711+
Returns a copy of the current Finch client with the given access token for
712+
authentication.
713+
"""
714+
return self.with_options(access_token=access_token)
715+
584716
@override
585717
def _make_status_error(
586718
self,

src/finch/resources/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
SandboxWithStreamingResponse,
3333
AsyncSandboxWithStreamingResponse,
3434
)
35+
from .webhooks import Webhooks, AsyncWebhooks
3536
from .providers import (
3637
Providers,
3738
AsyncProviders,
@@ -82,6 +83,8 @@
8283
"AsyncAccountWithRawResponse",
8384
"AccountWithStreamingResponse",
8485
"AsyncAccountWithStreamingResponse",
86+
"Webhooks",
87+
"AsyncWebhooks",
8588
"RequestForwarding",
8689
"AsyncRequestForwarding",
8790
"RequestForwardingWithRawResponse",

src/finch/resources/access_tokens.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
from .. import _legacy_response
88
from ..types import CreateAccessTokenResponse, access_token_create_params
99
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
10-
from .._utils import (
11-
maybe_transform,
12-
async_maybe_transform,
13-
)
10+
from .._utils import is_given, maybe_transform
1411
from .._compat import cached_property
1512
from .._resource import SyncAPIResource, AsyncAPIResource
1613
from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -56,13 +53,27 @@ def create(
5653
5754
timeout: Override the client-level default timeout for this request, in seconds
5855
"""
56+
if not is_given(client_id):
57+
if self._client.client_id is None:
58+
raise ValueError(
59+
"client_id must be provided as an argument or with the FINCH_CLIENT_ID environment variable"
60+
)
61+
client_id = self._client.client_id
62+
63+
if not is_given(client_secret):
64+
if self._client.client_secret is None:
65+
raise ValueError(
66+
"client_secret must be provided as an argument or with the FINCH_CLIENT_SECRET environment variable"
67+
)
68+
client_secret = self._client.client_secret
69+
5970
return self._post(
6071
"/auth/token",
6172
body=maybe_transform(
6273
{
63-
"code": code,
6474
"client_id": client_id,
6575
"client_secret": client_secret,
76+
"code": code,
6677
"redirect_uri": redirect_uri,
6778
},
6879
access_token_create_params.AccessTokenCreateParams,
@@ -109,13 +120,27 @@ async def create(
109120
110121
timeout: Override the client-level default timeout for this request, in seconds
111122
"""
123+
if not is_given(client_id):
124+
if self._client.client_id is None:
125+
raise ValueError(
126+
"client_id must be provided as an argument or with the FINCH_CLIENT_ID environment variable"
127+
)
128+
client_id = self._client.client_id
129+
130+
if not is_given(client_secret):
131+
if self._client.client_secret is None:
132+
raise ValueError(
133+
"client_secret must be provided as an argument or with the FINCH_CLIENT_SECRET environment variable"
134+
)
135+
client_secret = self._client.client_secret
136+
112137
return await self._post(
113138
"/auth/token",
114-
body=await async_maybe_transform(
139+
body=maybe_transform(
115140
{
116-
"code": code,
117141
"client_id": client_id,
118142
"client_secret": client_secret,
143+
"code": code,
119144
"redirect_uri": redirect_uri,
120145
},
121146
access_token_create_params.AccessTokenCreateParams,

0 commit comments

Comments
 (0)