Skip to content

Commit 6746539

Browse files
awalker4claude
andcommitted
Add async hook support to hook infrastructure
- Add AsyncSDKInitHook, AsyncBeforeRequestHook, AsyncAfterSuccessHook, and AsyncAfterErrorHook interfaces - Update SDKHooks to store and execute both sync and async hooks - Add async executor methods that run async hooks first, then sync hooks for backward compatibility - Update basesdk.py do_request_async to call async hook methods - Enables proper async/await patterns in hooks without blocking the event loop This is the first step toward fixing ENG-792 where blocking calls in the split PDF hook prevent concurrent async requests. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 46ea8f6 commit 6746539

File tree

3 files changed

+151
-7
lines changed

3 files changed

+151
-7
lines changed

src/unstructured_client/_hooks/sdkhooks.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT."""
22

33
import httpx
4+
import inspect
45
from .types import (
56
SDKInitHook,
67
BeforeRequestContext,
@@ -9,11 +10,15 @@
910
AfterSuccessHook,
1011
AfterErrorContext,
1112
AfterErrorHook,
13+
AsyncSDKInitHook,
14+
AsyncBeforeRequestHook,
15+
AsyncAfterSuccessHook,
16+
AsyncAfterErrorHook,
1217
Hooks,
1318
)
1419
from .registration import init_hooks
15-
from typing import List, Optional, Tuple
16-
from unstructured_client.httpclient import HttpClient
20+
from typing import List, Optional, Tuple, Union
21+
from unstructured_client.httpclient import AsyncHttpClient, HttpClient
1722

1823

1924
class SDKHooks(Hooks):
@@ -22,6 +27,10 @@ def __init__(self) -> None:
2227
self.before_request_hooks: List[BeforeRequestHook] = []
2328
self.after_success_hooks: List[AfterSuccessHook] = []
2429
self.after_error_hooks: List[AfterErrorHook] = []
30+
self.async_sdk_init_hooks: List[AsyncSDKInitHook] = []
31+
self.async_before_request_hooks: List[AsyncBeforeRequestHook] = []
32+
self.async_after_success_hooks: List[AsyncAfterSuccessHook] = []
33+
self.async_after_error_hooks: List[AsyncAfterErrorHook] = []
2534
init_hooks(self)
2635

2736
def register_sdk_init_hook(self, hook: SDKInitHook) -> None:
@@ -36,6 +45,18 @@ def register_after_success_hook(self, hook: AfterSuccessHook) -> None:
3645
def register_after_error_hook(self, hook: AfterErrorHook) -> None:
3746
self.after_error_hooks.append(hook)
3847

48+
def register_async_sdk_init_hook(self, hook: AsyncSDKInitHook) -> None:
49+
self.async_sdk_init_hooks.append(hook)
50+
51+
def register_async_before_request_hook(self, hook: AsyncBeforeRequestHook) -> None:
52+
self.async_before_request_hooks.append(hook)
53+
54+
def register_async_after_success_hook(self, hook: AsyncAfterSuccessHook) -> None:
55+
self.async_after_success_hooks.append(hook)
56+
57+
def register_async_after_error_hook(self, hook: AsyncAfterErrorHook) -> None:
58+
self.async_after_error_hooks.append(hook)
59+
3960
def sdk_init(self, base_url: str, client: HttpClient) -> Tuple[str, HttpClient]:
4061
for hook in self.sdk_init_hooks:
4162
base_url, client = hook.sdk_init(base_url, client)
@@ -74,3 +95,75 @@ def after_error(
7495
raise result
7596
response, error = result
7697
return response, error
98+
99+
async def sdk_init_async(
100+
self, base_url: str, client: AsyncHttpClient
101+
) -> Tuple[str, AsyncHttpClient]:
102+
# Run async hooks first
103+
for hook in self.async_sdk_init_hooks:
104+
base_url, client = await hook.sdk_init_async(base_url, client)
105+
# Then run sync hooks for backward compatibility (if hook has sync version)
106+
for hook in self.sdk_init_hooks:
107+
# Cast the client temporarily - sync hooks may not touch async client
108+
base_url, _ = hook.sdk_init(base_url, client) # type: ignore
109+
return base_url, client
110+
111+
async def before_request_async(
112+
self, hook_ctx: BeforeRequestContext, request: httpx.Request
113+
) -> httpx.Request:
114+
# Run async hooks first
115+
for hook in self.async_before_request_hooks:
116+
out = await hook.before_request_async(hook_ctx, request)
117+
if isinstance(out, Exception):
118+
raise out
119+
request = out
120+
121+
# Then run sync hooks for backward compatibility
122+
for hook in self.before_request_hooks:
123+
out = hook.before_request(hook_ctx, request)
124+
if isinstance(out, Exception):
125+
raise out
126+
request = out
127+
128+
return request
129+
130+
async def after_success_async(
131+
self, hook_ctx: AfterSuccessContext, response: httpx.Response
132+
) -> httpx.Response:
133+
# Run async hooks first
134+
for hook in self.async_after_success_hooks:
135+
out = await hook.after_success_async(hook_ctx, response)
136+
if isinstance(out, Exception):
137+
raise out
138+
response = out
139+
140+
# Then run sync hooks for backward compatibility
141+
for hook in self.after_success_hooks:
142+
out = hook.after_success(hook_ctx, response)
143+
if isinstance(out, Exception):
144+
raise out
145+
response = out
146+
147+
return response
148+
149+
async def after_error_async(
150+
self,
151+
hook_ctx: AfterErrorContext,
152+
response: Optional[httpx.Response],
153+
error: Optional[Exception],
154+
) -> Tuple[Optional[httpx.Response], Optional[Exception]]:
155+
# Run async hooks first
156+
for hook in self.async_after_error_hooks:
157+
result = await hook.after_error_async(hook_ctx, response, error)
158+
if isinstance(result, Exception):
159+
raise result
160+
response, error = result
161+
162+
# Then run sync hooks for backward compatibility
163+
for hook in self.after_error_hooks:
164+
result = hook.after_error(hook_ctx, response, error)
165+
if isinstance(result, Exception):
166+
raise result
167+
response, error = result
168+
169+
return response, error

src/unstructured_client/_hooks/types.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC, abstractmethod
44
import httpx
55
from typing import Any, Callable, List, Optional, Tuple, Union
6-
from unstructured_client.httpclient import HttpClient
6+
from unstructured_client.httpclient import AsyncHttpClient, HttpClient
77
from unstructured_client.sdkconfiguration import SDKConfiguration
88

99

@@ -95,6 +95,41 @@ def after_error(
9595
pass
9696

9797

98+
class AsyncSDKInitHook(ABC):
99+
@abstractmethod
100+
async def sdk_init_async(
101+
self, base_url: str, client: AsyncHttpClient
102+
) -> Tuple[str, AsyncHttpClient]:
103+
pass
104+
105+
106+
class AsyncBeforeRequestHook(ABC):
107+
@abstractmethod
108+
async def before_request_async(
109+
self, hook_ctx: BeforeRequestContext, request: httpx.Request
110+
) -> Union[httpx.Request, Exception]:
111+
pass
112+
113+
114+
class AsyncAfterSuccessHook(ABC):
115+
@abstractmethod
116+
async def after_success_async(
117+
self, hook_ctx: AfterSuccessContext, response: httpx.Response
118+
) -> Union[httpx.Response, Exception]:
119+
pass
120+
121+
122+
class AsyncAfterErrorHook(ABC):
123+
@abstractmethod
124+
async def after_error_async(
125+
self,
126+
hook_ctx: AfterErrorContext,
127+
response: Optional[httpx.Response],
128+
error: Optional[Exception],
129+
) -> Union[Tuple[Optional[httpx.Response], Optional[Exception]], Exception]:
130+
pass
131+
132+
98133
class Hooks(ABC):
99134
@abstractmethod
100135
def register_sdk_init_hook(self, hook: SDKInitHook):
@@ -111,3 +146,19 @@ def register_after_success_hook(self, hook: AfterSuccessHook):
111146
@abstractmethod
112147
def register_after_error_hook(self, hook: AfterErrorHook):
113148
pass
149+
150+
@abstractmethod
151+
def register_async_sdk_init_hook(self, hook: AsyncSDKInitHook):
152+
pass
153+
154+
@abstractmethod
155+
def register_async_before_request_hook(self, hook: AsyncBeforeRequestHook):
156+
pass
157+
158+
@abstractmethod
159+
def register_async_after_success_hook(self, hook: AsyncAfterSuccessHook):
160+
pass
161+
162+
@abstractmethod
163+
def register_async_after_error_hook(self, hook: AsyncAfterErrorHook):
164+
pass

src/unstructured_client/basesdk.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ async def do_request_async(
300300
async def do():
301301
http_res = None
302302
try:
303-
req = hooks.before_request(BeforeRequestContext(hook_ctx), request)
303+
req = await hooks.before_request_async(BeforeRequestContext(hook_ctx), request)
304304
logger.debug(
305305
"Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s",
306306
req.method,
@@ -314,7 +314,7 @@ async def do():
314314

315315
http_res = await client.send(req, stream=stream)
316316
except Exception as e:
317-
_, e = hooks.after_error(AfterErrorContext(hook_ctx), None, e)
317+
_, e = await hooks.after_error_async(AfterErrorContext(hook_ctx), None, e)
318318
if e is not None:
319319
logger.debug("Request Exception", exc_info=True)
320320
raise e
@@ -332,7 +332,7 @@ async def do():
332332
)
333333

334334
if utils.match_status_codes(error_status_codes, http_res.status_code):
335-
result, err = hooks.after_error(
335+
result, err = await hooks.after_error_async(
336336
AfterErrorContext(hook_ctx), http_res, None
337337
)
338338
if err is not None:
@@ -354,6 +354,6 @@ async def do():
354354
http_res = await do()
355355

356356
if not utils.match_status_codes(error_status_codes, http_res.status_code):
357-
http_res = hooks.after_success(AfterSuccessContext(hook_ctx), http_res)
357+
http_res = await hooks.after_success_async(AfterSuccessContext(hook_ctx), http_res)
358358

359359
return http_res

0 commit comments

Comments
 (0)