-
Notifications
You must be signed in to change notification settings - Fork 9
feat: Implement asynchronous HTTP client using httpx #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement asynchronous HTTP client using httpx #87
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughIntroduces an asynchronous HTTP client stack: adds AsyncHttpClient base class, implements MpesaAsyncHttpClient using httpx with async context management, error handling, and base URL resolution; updates package exports; adds dependencies and async test tooling; provides comprehensive unit tests; and applies a minor formatting tweak to the synchronous client. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App as Caller
participant AHC as MpesaAsyncHttpClient
participant HX as httpx.AsyncClient
participant API as M-Pesa API
App->>AHC: post(path, json, headers?)
activate AHC
AHC->>AHC: resolve base URL (sandbox|production)
AHC->>HX: POST base_url+path with json, headers, timeout
activate HX
HX->>API: HTTP POST
API-->>HX: HTTP response (status, body)
deactivate HX
AHC->>AHC: parse JSON or extract error text
alt status 2xx
AHC-->>App: Dict (parsed JSON)
else status >=400
AHC-->>App: raise MpesaApiException(code, MpesaError)
end
deactivate AHC
note over AHC,HX: Maps httpx timeouts/connect/HTTP errors to MpesaApiException with standardized error_code
App->>AHC: get(path, params?, headers?)
AHC->>HX: GET base_url+path with params, headers, timeout
HX->>API: HTTP GET
API-->>HX: HTTP response
AHC-->>App: JSON or raises MpesaApiException (as above)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (6)
mpesakit/http_client/__init__.py (2)
1-1: Add space after comma for PEP 8 compliance.Missing space after the comma between
HttpClientandAsyncHttpClient.Apply this diff:
-from .http_client import HttpClient,AsyncHttpClient +from .http_client import HttpClient, AsyncHttpClient
5-5: Add spaces after commas for PEP 8 compliance.Missing spaces after commas in the
__all__list.Apply this diff:
-__all__ = ["HttpClient", "MpesaHttpClient","AsyncHttpClient","MpesaAsyncHttpClient"] +__all__ = ["HttpClient", "MpesaHttpClient", "AsyncHttpClient", "MpesaAsyncHttpClient"]pyproject.toml (1)
48-48: LGTM! Consider updating minimum httpx version.The httpx dependency is correctly added. The version constraint
>=0.27.0,<1.0.0is reasonable and compatible with the async client implementation.Optionally, consider updating the minimum version to
>=0.28.0to benefit from the latest bug fixes and improvements in the 0.28.x series. Based on learnings, httpx 0.28.1 is the current stable release with maintenance fixes, and upgrades from 0.27.x are typically safe.If you choose to update:
- "httpx >=0.27.0,<1.0.0", + "httpx >=0.28.0,<1.0.0",mpesakit/http_client/mpesa_async_http_client.py (3)
5-5: Remove unused import.The
asyncioimport is not used anywhere in this file. While httpx handles async operations internally, this import is unnecessary.Apply this diff:
from typing import Dict, Any, Optional import httpx -import asyncio from mpesakit.errors import MpesaError, MpesaApiException
98-156: LGTM! Consider extracting common error handling.The async GET method is correctly implemented with proper response and exception handling.
The error handling logic (lines 132-156) is duplicated from the POST method (lines 71-96). Consider extracting this into a private method to reduce duplication:
def _handle_httpx_exception(self, exc: Exception) -> None: """Convert httpx exceptions to MpesaApiException.""" if isinstance(exc, httpx.TimeoutException): raise MpesaApiException( MpesaError( error_code="REQUEST_TIMEOUT", error_message="Request to Mpesa timed out.", status_code=None, ) ) elif isinstance(exc, httpx.ConnectError): raise MpesaApiException( MpesaError( error_code="CONNECTION_ERROR", error_message="Failed to connect to Mpesa API. Check network or URL.", status_code=None, ) ) elif isinstance(exc, httpx.HTTPError): raise MpesaApiException( MpesaError( error_code="REQUEST_FAILED", error_message=f"HTTP request failed: {str(exc)}", status_code=None, raw_response=None, ) )Then use try/except with
self._handle_httpx_exception(e)in both methods.
161-161: Consider adding a blank line at end of file.PEP 8 recommends files end with a blank line for better compatibility with some tools.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
mpesakit/http_client/__init__.py(1 hunks)mpesakit/http_client/http_client.py(1 hunks)mpesakit/http_client/mpesa_async_http_client.py(1 hunks)mpesakit/http_client/mpesa_http_client.py(1 hunks)pyproject.toml(3 hunks)tests/unit/http_client/test_mpesa_async_http_client.py(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
mpesakit/http_client/mpesa_async_http_client.py (3)
mpesakit/errors.py (3)
MpesaError(11-30)MpesaApiException(33-49)error_code(42-44)mpesakit/http_client/http_client.py (5)
AsyncHttpClient(30-53)post(14-18)post(39-43)get(21-28)get(46-53)mpesakit/http_client/mpesa_http_client.py (3)
_resolve_base_url(34-37)post(39-101)get(103-173)
mpesakit/http_client/__init__.py (2)
mpesakit/http_client/http_client.py (2)
HttpClient(10-28)AsyncHttpClient(30-53)mpesakit/http_client/mpesa_async_http_client.py (1)
MpesaAsyncHttpClient(11-156)
mpesakit/http_client/http_client.py (2)
mpesakit/http_client/mpesa_async_http_client.py (2)
post(42-96)get(98-156)mpesakit/http_client/mpesa_http_client.py (2)
post(39-101)get(103-173)
tests/unit/http_client/test_mpesa_async_http_client.py (2)
mpesakit/http_client/mpesa_async_http_client.py (3)
MpesaAsyncHttpClient(11-156)post(42-96)get(98-156)mpesakit/errors.py (2)
MpesaApiException(33-49)error_code(42-44)
🔇 Additional comments (22)
mpesakit/http_client/mpesa_http_client.py (1)
129-129: LGTM! Minor formatting cleanup.Removing the inline comment improves consistency with the POST method (line 57), which also specifies the timeout without a comment.
pyproject.toml (3)
69-70: LGTM!The addition of
types-requestsandpytest-asyncioto dev dependencies is correct. The pytest-asyncio version constraint>=0.23.6,<1.0.0ensures compatibility with async test features.
78-79: LGTM!The addition of
types-requestsandpytest-asyncioto test dependencies mirrors the dev dependencies and is appropriate for the async test suite.
126-127: LGTM!The
asynciomarker is correctly added to support async test execution with pytest-asyncio. This enables the@pytest.mark.asynciodecorator used in the test suite.mpesakit/http_client/http_client.py (1)
30-53: LGTM! Well-structured async base class.The
AsyncHttpClientabstract base class is correctly implemented with:
- Proper async method declarations for
postandget- Consistent method signatures matching the synchronous
HttpClientpattern- Appropriate use of
@abstractmethoddecoratorsThis provides a solid foundation for async HTTP client implementations.
tests/unit/http_client/test_mpesa_async_http_client.py (14)
16-22: LGTM!The fixture correctly patches
httpx.AsyncClientto prevent actual HTTP calls during unit tests. The use ofyieldproperly manages the fixture lifecycle.
25-28: LGTM!Correctly validates sandbox base URL resolution.
31-34: LGTM!Correctly validates production base URL resolution.
38-53: LGTM!Comprehensive test of successful async POST request, including:
- Proper mocking with AsyncMock
- Verification of return value
- Validation of call arguments including timeout
56-68: LGTM!Correctly validates HTTP error handling in POST requests, including proper exception type and error details.
71-84: LGTM!Excellent test coverage for JSON decode errors, validating the fallback to response.text when JSON parsing fails.
88-100: LGTM!Correctly validates timeout exception handling with proper error code mapping.
103-115: LGTM!Correctly validates connection error handling. The
request=Mock()parameter is properly provided for httpx.ConnectError construction.
118-131: LGTM!Correctly validates generic httpx.HTTPError handling using ProtocolError as a test case, with proper error message propagation.
134-146: LGTM!Comprehensive test of successful async GET request, properly validating return value and call arguments.
149-161: LGTM!Correctly validates HTTP error handling for GET requests with proper exception and error code verification.
163-176: LGTM!Correctly validates timeout handling for GET requests with proper error code and message verification.
179-192: LGTM!Correctly validates connection error handling for GET requests with proper exception construction and verification.
195-208: LGTM!Correctly validates generic httpx error handling for GET requests with proper error propagation.
mpesakit/http_client/mpesa_async_http_client.py (3)
11-32: LGTM!The class initialization correctly:
- Inherits from AsyncHttpClient
- Resolves environment-specific base URLs consistently with MpesaHttpClient
- Instantiates httpx.AsyncClient with the resolved base URL
35-39: LGTM!The async context manager is correctly implemented, enabling proper resource cleanup with
async withsyntax.
42-96: LGTM! Well-structured async POST implementation.The async POST method correctly:
- Implements the AsyncHttpClient interface
- Handles response parsing with fallback for non-JSON responses
- Maps httpx exceptions to MpesaApiException with appropriate error codes
- Uses httpx's
is_successproperty for status validation- Maintains consistency with the synchronous client's error handling
| async def aclose(self): | ||
| """Manually close the underlying httpx client connection pool.""" | ||
| await self._client.aclose() No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix critical indentation error.
The aclose method is defined at module level instead of as a class method due to missing indentation. This will prevent users from calling client.aclose() and cause an AttributeError.
Apply this diff to fix the indentation:
-async def aclose(self):
- """Manually close the underlying httpx client connection pool."""
- await self._client.aclose()
+ async def aclose(self):
+ """Manually close the underlying httpx client connection pool."""
+ await self._client.aclose()Note: While this method provides an explicit way to close the client, the async context manager (__aexit__) already handles cleanup. Consider whether this method is necessary, or if it's intended for cases where the client is used without the context manager.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async def aclose(self): | |
| """Manually close the underlying httpx client connection pool.""" | |
| await self._client.aclose() | |
| async def aclose(self): | |
| """Manually close the underlying httpx client connection pool.""" | |
| await self._client.aclose() |
🤖 Prompt for AI Agents
In mpesakit/http_client/mpesa_async_http_client.py around lines 159 to 161, the
async def aclose is defined at module level instead of as a class method; indent
the entire aclose method one level so it is inside the client class (so its
signature remains async def aclose(self): and body awaits
self._client.aclose()), keeping the docstring, and run tests to ensure
client.aclose() is available; optionally remove or document it if you prefer
relying solely on __aexit__.
|
Hey @watersRand , thanks for another PR, Looks Good for now, would you kindly address the issue by CodeRabbit above, then we would be good to go 👍 consider checking this comment and this comment |
| ) # Add timeout | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let this comment be for now,
for explicit tracking, you can open another PR or maybe add it in its own commit message.
375ef15 to
62cc29e
Compare
62cc29e to
5124c8c
Compare
Its not a part of the issues tagged.
RafaelJohn9
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome thank you @watersRand , for another wholesome PR
Feature: Asynchronous HTTP Client Implementation
Description
This Pull Request introduces a major new feature by implementing a fully asynchronous HTTP client structure for the Mpesa SDK. This change enables non-blocking I/O operations, addressing the need for efficient integration within modern Python asynchronous web frameworks and applications (e.g., FastAPI, Django Async Views, Starlette).
The core motivation is to enhance the SDK's performance and scalability in concurrent environments. By utilizing the
httpxlibrary, we ensure the client yields control back to the event loop while waiting for network responses, maximizing concurrency without consuming multiple threads.This PR establishes the foundation for all future asynchronous M-Pesa API method implementations.
Currently Closes #55 and ##56
Type of Change
How Has This Been Tested?
The implementation was rigorously tested using the following methods:
test_mpesa_async_http_client.py, was created to test theMpesaAsyncHttpClientclass.async postandasync getrequests.httpxandAsyncMockto verify correct asynchronous exception handling, including:httpx.TimeoutExceptionhttpx.ConnectErrorhttpx.HTTPError(Protocol, Request, etc.)MpesaAsyncHttpClientcorrectly inherits from the new abstract base classAsyncHttpClientand that the originalMpesaHttpClientis unaffected, maintaining backwards compatibility.httpxis properly listed as a dependency, andpytest-asynciois correctly configured for test execution.Checklist
This PR serves as the foundational layer. Subsequent feature PRs will build upon this by implementing asynchronous service methods (e.g.,
async_stk_push,async_b2c).The
MpesaAsyncHttpClientalso implements the__aenter__and__aexit__methods, allowing it to be used efficiently as an asynchronous context manager (async with client:), which is the recommended practice for managinghttpx.AsyncClientresources.Summary by CodeRabbit
New Features
Tests
Chores