Skip to content

Commit 030cb90

Browse files
committed
Revert "Merge pull request #119 from Byte-Barn/revert-118-refactor/requests-to-httpx"
This reverts commit 7e2fd0a, reversing changes made to e9f0856.
1 parent d8d4351 commit 030cb90

4 files changed

Lines changed: 234 additions & 275 deletions

File tree

mpesakit/errors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
It uses Pydantic for validation and serialization of error data.
55
"""
66

7-
from typing import Dict, Any, Optional
7+
from typing import Any, Optional
8+
89
from pydantic import BaseModel, Field
910

1011

@@ -15,7 +16,7 @@ class MpesaError(BaseModel):
1516
error_code: Optional[str] = Field(default=None)
1617
error_message: Optional[str] = Field(default=None)
1718
status_code: Optional[int] = Field(default=None)
18-
raw_response: Optional[Dict[str, Any]] = Field(default=None)
19+
raw_response: Optional[Any] = Field(default=None)
1920

2021
def __str__(self) -> str:
2122
parts = []

mpesakit/http_client/mpesa_http_client.py

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Dict, Optional
88
from urllib.parse import urljoin
99

10-
import requests
10+
import httpx
1111
from tenacity import (
1212
RetryCallState,
1313
before_sleep_log,
@@ -24,13 +24,13 @@
2424
logger = 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

Comments
 (0)