Skip to content

Conversation

@watersRand
Copy link
Contributor

@watersRand watersRand commented Oct 9, 2025

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 httpx library, 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires documentation update
  • Refactor (code structure improvements, no new functionality)
  • Tests (addition or improvement of tests)
  • Chore (changes to tooling, CI/CD, or metadata)

How Has This Been Tested?

The implementation was rigorously tested using the following methods:

  1. Unit Tests: A new dedicated file, test_mpesa_async_http_client.py, was created to test the MpesaAsyncHttpClient class.
    • Tests cover successful async post and async get requests.
    • Tests extensively mock httpx and AsyncMock to verify correct asynchronous exception handling, including:
      • httpx.TimeoutException
      • httpx.ConnectError
      • Generic httpx.HTTPError (Protocol, Request, etc.)
      • M-Pesa API HTTP errors (4xx/5xx responses)
      • JSON decoding errors from non-JSON responses.
  2. Architectural Check: Verified that the new MpesaAsyncHttpClient correctly inherits from the new abstract base class AsyncHttpClient and that the original MpesaHttpClient is unaffected, maintaining backwards compatibility.
  3. Dependency Check: Confirmed that httpx is properly listed as a dependency, and pytest-asyncio is correctly configured for test execution.

Checklist

  • My code follows the project's coding style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas (e.g., the exception handling blocks)
  • I have made corresponding changes to the documentation (if applicable - will be done in a follow-up PR)
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

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 MpesaAsyncHttpClient also 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 managing httpx.AsyncClient resources.

Summary by CodeRabbit

  • New Features

    • Introduced an asynchronous HTTP client for M‑Pesa with async GET/POST support, context management, and improved error handling.
    • Exposed async client classes via package exports for easy import.
  • Tests

    • Added comprehensive unit tests covering async success paths, error handling, timeouts, and base URL resolution.
  • Chores

    • Added httpx as a dependency.
    • Added pytest-asyncio to dev/test setup and updated test markers to include “asyncio.”

@coderabbitai
Copy link

coderabbitai bot commented Oct 9, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Introduces 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

Cohort / File(s) Summary
Async base + exports
mpesakit/http_client/__init__.py, mpesakit/http_client/http_client.py
Adds AsyncHttpClient abstract base with get/post async methods and exports it alongside MpesaAsyncHttpClient.
Async M-Pesa client + tests
mpesakit/http_client/mpesa_async_http_client.py, tests/unit/http_client/test_mpesa_async_http_client.py
Implements MpesaAsyncHttpClient using httpx.AsyncClient, with env-based base URL, async context management, GET/POST, and standardized error mapping; adds extensive async unit tests covering success and error scenarios.
Sync client minor edit
mpesakit/http_client/mpesa_http_client.py
Removes an inline comment in GET; no functional changes.
Project configuration
pyproject.toml
Adds httpx dependency, introduces pytest-asyncio to dev/test extras, adjusts test markers, and minor formatting.

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

I thump my paws in async delight,
Hopping through sockets by moonlit night.
Sandbox to prod, I sniff the trail—
If errors loom, I twitch my tail.
With httpx winds beneath my ear,
I fetch and post without a fear.
Boop! Success is crispy-clear. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The linked issue #55 only specifies the addition of an abstract AsyncHttpClient base class and its export in init.py. This PR also introduces a full MpesaAsyncHttpClient implementation, dependency updates, tests, and minor formatting changes that exceed the scope defined by the issue. These additional components are not covered by the linked issue and thus represent out-of-scope changes. To align with the linked issue scope, consider segregating the MpesaAsyncHttpClient implementation, dependency modifications, and test additions into separate PRs linked to their respective issues or broaden the issue description to include these components.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly states the addition of an asynchronous HTTP client and references the httpx library. It succinctly captures the main feature implemented without extraneous detail. This clarity ensures that teammates can understand the primary change when scanning PR history.
Linked Issues Check ✅ Passed The PR adds the AsyncHttpClient abstract base class in the correct module and updates the package init.py to include it, directly satisfying the coding requirements of issue #55. All specified methods and export declarations are present and correctly implemented. No other coding tasks related to this issue remain outstanding.
Docstring Coverage ✅ Passed Docstring coverage is 88.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed The pull request description comprehensively addresses all key template sections. The Description section clearly explains the feature (asynchronous HTTP client), motivates the change (non-blocking I/O for async frameworks), and references related issues. The Type of Change section appropriately checks four options (New feature, Refactor, Tests, and Chore), all of which align with the actual changes shown in the code summary. The "How Has This Been Tested?" section provides detailed testing methodology including unit tests, exception handling validation, architectural verification, and dependency confirmation. The Checklist is nearly complete with seven of eight items checked, and the two unchecked items include reasonable explanations (documentation deferred to follow-up PR, no downstream dependencies). Additional Context explains the foundational nature and async context manager implementation.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 HttpClient and AsyncHttpClient.

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.0 is reasonable and compatible with the async client implementation.

Optionally, consider updating the minimum version to >=0.28.0 to 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 asyncio import 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1fbd53b and cd5b3b7.

📒 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-requests and pytest-asyncio to dev dependencies is correct. The pytest-asyncio version constraint >=0.23.6,<1.0.0 ensures compatibility with async test features.


78-79: LGTM!

The addition of types-requests and pytest-asyncio to test dependencies mirrors the dev dependencies and is appropriate for the async test suite.


126-127: LGTM!

The asyncio marker is correctly added to support async test execution with pytest-asyncio. This enables the @pytest.mark.asyncio decorator used in the test suite.

mpesakit/http_client/http_client.py (1)

30-53: LGTM! Well-structured async base class.

The AsyncHttpClient abstract base class is correctly implemented with:

  • Proper async method declarations for post and get
  • Consistent method signatures matching the synchronous HttpClient pattern
  • Appropriate use of @abstractmethod decorators

This 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.AsyncClient to prevent actual HTTP calls during unit tests. The use of yield properly 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 with syntax.


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_success property for status validation
  • Maintains consistency with the synchronous client's error handling

Comment on lines 159 to 161
async def aclose(self):
"""Manually close the underlying httpx client connection pool."""
await self._client.aclose() No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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__.

@RafaelJohn9
Copy link
Member

RafaelJohn9 commented Oct 14, 2025

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

Comment on lines 129 to 130
) # Add timeout
)

Copy link
Member

@RafaelJohn9 RafaelJohn9 Oct 14, 2025

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.

@RafaelJohn9 RafaelJohn9 changed the base branch from master to dev October 14, 2025 07:52
@RafaelJohn9 RafaelJohn9 changed the base branch from dev to master October 14, 2025 08:01
@RafaelJohn9 RafaelJohn9 changed the base branch from master to develop October 14, 2025 08:38
@watersRand watersRand force-pushed the feature/AsyncHttpClient branch 2 times, most recently from 375ef15 to 62cc29e Compare October 18, 2025 07:34
@watersRand watersRand force-pushed the feature/AsyncHttpClient branch from 62cc29e to 5124c8c Compare October 18, 2025 07:45
Its not a part of the issues tagged.
Copy link
Member

@RafaelJohn9 RafaelJohn9 left a 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

@RafaelJohn9 RafaelJohn9 merged commit 900b2b7 into Byte-Barn:develop Oct 21, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Abstract base AsyncHttpClient

2 participants