77from typing import Any , Dict , Optional
88from urllib .parse import urljoin
99
10- import requests
10+ import httpx
1111from tenacity import (
1212 RetryCallState ,
1313 before_sleep_log ,
2424logger = logging .getLogger (__name__ )
2525
2626
27- def handle_request_error (response : requests .Response ):
27+ def handle_request_error (response : httpx .Response ):
2828 """Handles non-successful HTTP responses.
2929
3030 This function is now responsible for converting HTTP status codes
3131 and JSON parsing errors into MpesaApiException.
3232 """
33- if response .ok :
33+ if response .is_success :
3434 return
3535 try :
3636 response_data = response .json ()
@@ -56,11 +56,11 @@ def handle_retry_exception(retry_state: RetryCallState):
5656 if retry_state .outcome :
5757 exception = retry_state .outcome .exception ()
5858
59- if isinstance (exception , requests . exceptions . Timeout ):
59+ if isinstance (exception , httpx . TimeoutException ):
6060 raise MpesaApiException (
6161 MpesaError (error_code = "REQUEST_TIMEOUT" , error_message = str (exception ))
6262 ) from exception
63- elif isinstance (exception , requests . exceptions . ConnectionError ):
63+ elif isinstance (exception , httpx . ConnectError ):
6464 raise MpesaApiException (
6565 MpesaError (error_code = "CONNECTION_ERROR" , error_message = str (exception ))
6666 ) from exception
@@ -87,8 +87,8 @@ def retry_enabled(enabled: bool):
8787 A retry condition function.
8888 """
8989 base_retry = retry_if_exception_type (
90- requests . exceptions . Timeout
91- ) | retry_if_exception_type (requests . exceptions . ConnectionError )
90+ httpx . TimeoutException
91+ ) | retry_if_exception_type (httpx . ConnectError )
9292
9393 def _retry (retry_state ):
9494 if not enabled :
@@ -102,7 +102,7 @@ class MpesaHttpClient(HttpClient):
102102 """A client for making HTTP requests to the M-Pesa API."""
103103
104104 base_url : str
105- _session : Optional [requests . Session ] = None
105+ _client : Optional [httpx . Client ] = None
106106
107107 def __init__ (
108108 self , env : str = "sandbox" , use_session : bool = False , trust_env : bool = True
@@ -111,13 +111,12 @@ def __init__(
111111
112112 Args:
113113 env (str): The environment to connect to ('sandbox' or 'production').
114- use_session (bool): Whether to use a persistent session .
115- trust_env (bool): Whether to trust environment proxy/CA settings (only applies in session mode) .
114+ use_session (bool): Whether to use a persistent client .
115+ trust_env (bool): Whether to trust environment proxy/CA settings.
116116 """
117117 self .base_url = self ._resolve_base_url (env )
118118 if use_session :
119- self ._session = requests .Session ()
120- self ._session .trust_env = trust_env
119+ self ._client = httpx .Client (trust_env = trust_env )
121120
122121 def _resolve_base_url (self , env : str ) -> str :
123122 if env .lower () == "production" :
@@ -133,15 +132,18 @@ def _resolve_base_url(self, env: str) -> str:
133132 )
134133 def _raw_post (
135134 self , url : str , json : Dict [str , Any ], headers : Dict [str , str ], timeout : int = 10
136- ) -> requests .Response :
137- """Low-level POST request - may raise requests exceptions."""
135+ ) -> httpx .Response :
136+ """Low-level POST request - may raise httpx exceptions."""
138137 full_url = urljoin (self .base_url , url )
139- if self ._session :
140- return self ._session .post (
138+ if self ._client :
139+ return self ._client .post (
141140 full_url , json = json , headers = headers , timeout = timeout
142141 )
143142 else :
144- return requests .post (full_url , json = json , headers = headers , timeout = timeout )
143+ with httpx .Client () as client :
144+ return client .post (
145+ full_url , json = json , headers = headers , timeout = timeout
146+ )
145147
146148 def post (
147149 self , url : str , json : Dict [str , Any ], headers : Dict [str , str ], timeout : int = 10
@@ -157,12 +159,12 @@ def post(
157159 Returns:
158160 Dict[str, Any]: The JSON response from the API.
159161 """
160- response : requests .Response | None = None
162+ response : httpx .Response | None = None
161163 try :
162164 response = self ._raw_post (url , json , headers , timeout )
163165 handle_request_error (response )
164166 return response .json ()
165- except (requests . RequestException , ValueError ) as e :
167+ except (httpx . RequestError , ValueError ) as e :
166168 raise MpesaApiException (
167169 MpesaError (
168170 error_code = "REQUEST_FAILED" ,
@@ -184,40 +186,46 @@ def _raw_get(
184186 url : str ,
185187 params : Optional [Dict [str , Any ]] = None ,
186188 headers : Optional [Dict [str , str ]] = None ,
187- ) -> requests .Response :
188- """Low-level GET request - may raise requests exceptions."""
189+ timeout : int = 10 ,
190+ ) -> httpx .Response :
191+ """Low-level GET request - may raise httpx exceptions."""
189192 if headers is None :
190193 headers = {}
191194 full_url = urljoin (self .base_url , url )
192- if self ._session :
193- return self ._session .get (
194- full_url , params = params , headers = headers , timeout = 10
195+ if self ._client :
196+ return self ._client .get (
197+ full_url , params = params , headers = headers , timeout = timeout
195198 )
196199 else :
197- return requests .get (full_url , params = params , headers = headers , timeout = 10 )
200+ with httpx .Client () as client :
201+ return client .get (
202+ full_url , params = params , headers = headers , timeout = timeout
203+ )
198204
199205 def get (
200206 self ,
201207 url : str ,
202208 params : Optional [Dict [str , Any ]] = None ,
203209 headers : Optional [Dict [str , str ]] = None ,
210+ timeout : int = 10 ,
204211 ) -> Dict [str , Any ]:
205212 """Sends a GET request to the M-Pesa API.
206213
207214 Args:
208215 url (str): The URL path for the request.
209216 params (Optional[Dict[str, Any]]): The URL parameters.
210217 headers (Optional[Dict[str, str]]): The HTTP headers.
218+ timeout (int): The timeout for the request in seconds.
211219
212220 Returns:
213221 Dict[str, Any]: The JSON response from the API.
214222 """
215- response : requests .Response | None = None
223+ response : httpx .Response | None = None
216224 try :
217- response = self ._raw_get (url , params , headers )
225+ response = self ._raw_get (url , params , headers , timeout )
218226 handle_request_error (response )
219227 return response .json ()
220- except (requests . RequestException , ValueError ) as e :
228+ except (httpx . RequestError , ValueError ) as e :
221229 raise MpesaApiException (
222230 MpesaError (
223231 error_code = "REQUEST_FAILED" ,
@@ -228,15 +236,15 @@ def get(
228236 ) from e
229237
230238 def close (self ) -> None :
231- """Closes the persistent session if it exists."""
232- if self ._session :
233- self ._session .close ()
234- self ._session = None
239+ """Closes the persistent client if it exists."""
240+ if self ._client :
241+ self ._client .close ()
242+ self ._client = None
235243
236244 def __enter__ (self ) -> "MpesaHttpClient" :
237245 """Context manager entry point."""
238246 return self
239247
240248 def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
241- """Context manager exit point. Closes the session ."""
249+ """Context manager exit point. Closes the client ."""
242250 self .close ()
0 commit comments