22This module provides functionality for handling HTTP requests in the Typesense client library.
33
44Classes:
5- - RequestHandler: Manages HTTP requests to the Typesense API.
5+ - RequestHandler: Manages HTTP requests to the Typesense API (supports both sync and async) .
66 - SessionFunctionKwargs: Type for keyword arguments in session functions.
77
88The RequestHandler class interacts with the Typesense API to manage HTTP requests,
99handle authentication, and process responses. It provides methods to send requests,
10- normalize parameters, and handle errors.
10+ normalize parameters, and handle errors. It supports both sync (httpx.Client) and async (httpx.AsyncClient) clients.
1111
1212This module uses type hinting and is compatible with Python 3.11+ as well as earlier
1313versions through the use of the typing_extensions library.
1717- Supports JSON and non-JSON responses
1818- Provides custom error handling for various HTTP status codes
1919- Normalizes boolean parameters for API requests
20+ - Supports both sync (httpx.Client) and async (httpx.AsyncClient) HTTP clients
2021
21- Note: This module relies on the 'requests ' library for making HTTP requests .
22+ Note: This module relies on the 'httpx ' library for both sync and async operations .
2223"""
2324
2425import json
2526import sys
2627from types import MappingProxyType
2728
28- import requests
29+ import httpx
2930
3031if sys .version_info >= (3 , 11 ):
3132 import typing
4748)
4849
4950TEntityDict = typing .TypeVar ("TEntityDict" )
50- TParams = typing .TypeVar ("TParams" )
51- TBody = typing .TypeVar ("TBody" )
51+ TParams = typing .TypeVar ("TParams" , bound = typing . Dict [ str , typing . Any ] )
52+ TBody = typing .TypeVar ("TBody" , bound = typing . Union [ str , bytes ] )
5253
5354_ERROR_CODE_MAP : typing .Mapping [str , typing .Type [TypesenseClientError ]] = (
5455 MappingProxyType (
6970
7071class SessionFunctionKwargs (typing .Generic [TParams , TBody ], typing .TypedDict ):
7172 """
72- Type definition for keyword arguments used in session functions.
73+ Type definition for keyword arguments used in request functions.
74+
75+ This is an internal abstraction that gets converted to httpx's request parameters.
76+ The `data` field is converted to `content` when passed to httpx.
77+
78+ Note: `verify` and `timeout` are set on the httpx client, not in request kwargs.
79+ However, we include them here for compatibility with the existing API.
7380
7481 Attributes:
7582 params (Optional[Union[TParams, None]]): Query parameters for the request.
83+ Passed as `params` to httpx.
7684
7785 data (Optional[Union[TBody, str, None]]): Body of the request.
86+ Converted to `content` (JSON string) when passed to httpx.
7887
7988 headers (Optional[Dict[str, str]]): Headers for the request.
89+ Passed as `headers` to httpx.
8090
8191 timeout (float): Timeout for the request in seconds.
92+ Set on the httpx client, not in request kwargs.
8293
8394 verify (bool): Whether to verify SSL certificates.
95+ Set on the httpx client, not in request kwargs.
8496 """
8597
8698 params : typing .NotRequired [typing .Union [TParams , None ]]
87- data : typing .NotRequired [typing .Union [TBody , str , None ]]
99+ data : typing .NotRequired [
100+ typing .Union [TBody , str , typing .Dict [str , typing .Any ], None ]
101+ ]
102+ content : typing .NotRequired [typing .Union [TBody , str , None ]]
88103 headers : typing .NotRequired [typing .Dict [str , str ]]
89- timeout : float
90- verify : bool
104+ timeout : typing .NotRequired [float ]
91105
92106
93107class RequestHandler :
94108 """
95- Handles HTTP requests to the Typesense API.
109+ Handles HTTP requests to the Typesense API (supports both sync and async using httpx) .
96110
97111 This class manages authentication, request sending, and response processing
98- for interactions with the Typesense API.
112+ for interactions with the Typesense API. It can work with both sync (httpx.Client)
113+ and async (httpx.AsyncClient) HTTP clients.
99114
100115 Attributes:
101116 api_key_header_name (str): The header name for the API key.
@@ -113,109 +128,130 @@ def __init__(self, config: Configuration):
113128 """
114129 self .config = config
115130
116- @typing .overload
117131 def make_request (
118132 self ,
119- fn : typing .Callable [..., requests .models .Response ],
133+ * ,
134+ method : str ,
120135 url : str ,
121136 entity_type : typing .Type [TEntityDict ],
122- as_json : typing .Literal [False ],
137+ as_json : typing .Union [typing .Literal [True ], typing .Literal [False ]] = True ,
138+ client : typing .Union [httpx .Client , httpx .AsyncClient ],
123139 ** kwargs : typing .Unpack [SessionFunctionKwargs [TParams , TBody ]],
124- ) -> str :
140+ ) -> typing .Union [
141+ TEntityDict ,
142+ str ,
143+ typing .Coroutine [typing .Any , typing .Any , typing .Union [TEntityDict , str ]],
144+ ]:
125145 """
126- Make an HTTP request to the Typesense API and return the response as a string.
127-
128- This overload is used when as_json is set to False, indicating that the response
129- should be returned as a raw string instead of being parsed as JSON.
146+ Make an HTTP request to the Typesense API (supports both sync and async using httpx).
130147
131148 Args:
132- fn (Callable ): The HTTP method function to use (e.g., requests.get ).
149+ method (str ): The HTTP method (e.g., "GET", "POST", "PUT", "PATCH", "DELETE" ).
133150
134151 url (str): The URL to send the request to.
135152
136153 entity_type (Type[TEntityDict]): The expected type of the response entity.
137154
138- as_json (Literal[False]): Specifies that the response should not be parsed as JSON.
155+ as_json (bool): Whether to return the response as JSON. Defaults to True.
156+
157+ client: The httpx client to use (httpx.Client for sync, httpx.AsyncClient for async).
139158
140159 kwargs: Additional keyword arguments for the request.
141160
142161 Returns:
143- str: The raw string response from the API.
162+ Union[TEntityDict, str]: The response, either as a JSON object or a string.
163+ If using AsyncClient, returns a coroutine.
144164
145165 Raises:
146166 TypesenseClientError: If the API returns an error response.
147167 """
168+ headers = {
169+ self .api_key_header_name : self .config .api_key ,
170+ }
171+ headers .update (self .config .additional_headers )
148172
149- @typing .overload
150- def make_request (
173+ request_kwargs : SessionFunctionKwargs [TParams , TBody ] = typing .cast (
174+ SessionFunctionKwargs [TParams , TBody ],
175+ {
176+ "headers" : headers ,
177+ "timeout" : self .config .connection_timeout_seconds ,
178+ },
179+ )
180+
181+ if params := kwargs .get ("params" ):
182+ self .normalize_params (params )
183+ request_kwargs ["params" ] = params
184+
185+ if body := kwargs .get ("data" ):
186+ if not isinstance (body , (str , bytes )):
187+ body = json .dumps (body )
188+ request_kwargs ["content" ] = typing .cast (TBody , body )
189+
190+ if isinstance (client , httpx .AsyncClient ):
191+ return self ._make_async_request (
192+ method , url , entity_type , as_json , client , ** request_kwargs
193+ )
194+ else :
195+ return self ._make_sync_request (
196+ method , url , entity_type , as_json , client , ** request_kwargs
197+ )
198+
199+ def _make_sync_request (
151200 self ,
152- fn : typing . Callable [..., requests . models . Response ] ,
201+ method : str ,
153202 url : str ,
154203 entity_type : typing .Type [TEntityDict ],
155- as_json : typing .Literal [True ],
156- ** kwargs : typing .Unpack [SessionFunctionKwargs [TParams , TBody ]],
157- ) -> TEntityDict :
158- """
159- Make an HTTP request to the Typesense API.
160-
161- Args:
162- fn (Callable): The HTTP method function to use (e.g., requests.get).
163-
164- url (str): The URL to send the request to.
165-
166- entity_type (Type[TEntityDict]): The expected type of the response entity.
167-
168- as_json (bool): Whether to return the response as JSON. Defaults to True.
204+ as_json : bool ,
205+ client : httpx .Client ,
206+ ** request_kwargs : typing .Unpack [SessionFunctionKwargs [TParams , TBody ]],
207+ ) -> typing .Union [TEntityDict , str ]:
208+ """Make a synchronous HTTP request using httpx.Client."""
209+ params : typing .Union [TParams , None ] = request_kwargs .get ("params" )
210+ content : typing .Union [TBody , str , None ] = request_kwargs .get ("content" )
211+ headers : typing .Dict [str , str ] = request_kwargs .get ("headers" , {})
212+
213+ response = client .request (
214+ method ,
215+ url ,
216+ params = params ,
217+ content = content ,
218+ headers = headers ,
219+ )
169220
170- kwargs: Additional keyword arguments for the request.
221+ if response .status_code < 200 or response .status_code >= 300 :
222+ error_message = self ._get_error_message (response )
223+ raise self ._get_exception (response .status_code )(
224+ response .status_code ,
225+ error_message ,
226+ )
171227
172- Returns:
173- TEntityDict: The response, as a JSON object.
228+ if as_json :
229+ res : TEntityDict = typing .cast (TEntityDict , response .json ())
230+ return res
174231
175- Raises:
176- TypesenseClientError: If the API returns an error response.
177- """
232+ return response .text
178233
179- def make_request (
234+ async def _make_async_request (
180235 self ,
181- fn : typing . Callable [..., requests . models . Response ] ,
236+ method : str ,
182237 url : str ,
183238 entity_type : typing .Type [TEntityDict ],
184- as_json : typing .Union [typing .Literal [True ], typing .Literal [False ]] = True ,
185- ** kwargs : typing .Unpack [SessionFunctionKwargs [TParams , TBody ]],
239+ as_json : bool ,
240+ client : httpx .AsyncClient ,
241+ ** request_kwargs : typing .Unpack [SessionFunctionKwargs [TParams , TBody ]],
186242 ) -> typing .Union [TEntityDict , str ]:
187- """
188- Make an HTTP request to the Typesense API.
189-
190- Args:
191- fn (Callable): The HTTP method function to use (e.g., requests.get).
192-
193- url (str): The URL to send the request to.
194-
195- entity_type (Type[TEntityDict]): The expected type of the response entity.
196-
197- as_json (bool): Whether to return the response as JSON. Defaults to True.
198-
199- kwargs: Additional keyword arguments for the request.
200-
201- Returns:
202- Union[TEntityDict, str]: The response, either as a JSON object or a string.
203-
204- Raises:
205- TypesenseClientError: If the API returns an error response.
206- """
207- headers = {
208- self .api_key_header_name : self .config .api_key ,
209- }
210- headers .update (self .config .additional_headers )
211-
212- kwargs .setdefault ("headers" , {}).update (headers )
213- kwargs .setdefault ("timeout" , self .config .connection_timeout_seconds )
214- kwargs .setdefault ("verify" , self .config .verify )
215- if kwargs .get ("data" ) and not isinstance (kwargs ["data" ], (str , bytes )):
216- kwargs ["data" ] = json .dumps (kwargs ["data" ])
217-
218- response = fn (url , ** kwargs )
243+ """Make an asynchronous HTTP request using httpx.AsyncClient."""
244+ params : typing .Union [TParams , None ] = request_kwargs .get ("params" )
245+ content : typing .Union [TBody , str , None ] = request_kwargs .get ("content" )
246+ headers : typing .Dict [str , str ] = request_kwargs .get ("headers" , {})
247+
248+ response = await client .request (
249+ method ,
250+ url ,
251+ params = params ,
252+ content = content ,
253+ headers = headers ,
254+ )
219255
220256 if response .status_code < 200 or response .status_code >= 300 :
221257 error_message = self ._get_error_message (response )
@@ -225,18 +261,18 @@ def make_request(
225261 )
226262
227263 if as_json :
228- res : TEntityDict = response .json ()
264+ res : TEntityDict = typing . cast ( TEntityDict , response .json () )
229265 return res
230266
231267 return response .text
232268
233269 @staticmethod
234- def normalize_params (params : TParams ) -> None :
270+ def normalize_params (params : typing . Dict [ str , typing . Any ] ) -> None :
235271 """
236272 Normalize boolean parameters in the request.
237273
238274 Args:
239- params (TParams ): The parameters to normalize.
275+ params (Dict[str, Any] ): The parameters to normalize.
240276
241277 Raises:
242278 ValueError: If params is not a dictionary.
@@ -248,12 +284,12 @@ def normalize_params(params: TParams) -> None:
248284 params [key ] = str (parameter_value ).lower ()
249285
250286 @staticmethod
251- def _get_error_message (response : requests .Response ) -> str :
287+ def _get_error_message (response : httpx .Response ) -> str :
252288 """
253289 Extract the error message from an API response.
254290
255291 Args:
256- response (requests .Response): The API response.
292+ response (httpx .Response): The API response.
257293
258294 Returns:
259295 str: The extracted error message or a default message.
@@ -262,9 +298,11 @@ def _get_error_message(response: requests.Response) -> str:
262298 if content_type .startswith ("application/json" ):
263299 try :
264300 return typing .cast (str , response .json ().get ("message" , "API error." ))
265- except requests . exceptions . JSONDecodeError :
301+ except ( json . JSONDecodeError , httpx . DecodingError ) :
266302 return f"API error: Invalid JSON response: { response .text } "
267- return "API error."
303+ if response .text :
304+ return f"API error. { response .text } "
305+ return f"Unknown API error. Full Response: { response } "
268306
269307 @staticmethod
270308 def _get_exception (http_code : int ) -> typing .Type [TypesenseClientError ]:
0 commit comments