Conversation
This commit introduces a robust HTTP client with optional session management. Users can now enable requests.Session to improve performance by reusing TCP connections for consecutive API calls. Adds unit tests to cover both session-based and stateless client behaviors. Refactors MpesaHttpClient to accept a use_session flag, with the default remaining as a stateless client.
…sakit into feat/add-sessions
Feat/add sessions
…(resolves review comment)
…esaHttpClient closes#44
Retry function
📝 WalkthroughWalkthroughIntroduces tenacity-based retry logic and centralized error handling to MpesaHttpClient, adds optional persistent requests.Session via a new use_session flag, updates MpesaClient to forward use_session, and expands tests to exercise session/non-session modes and retry behaviors. pyproject.toml dependency constraints updated. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Caller
participant MpesaClient
participant MpesaHttpClient
participant Tenacity as "tenacity (retry)"
participant Session as "requests.Session"
participant Requests as "requests"
Caller->>MpesaClient: post/get(path, payload, headers)
MpesaClient->>MpesaHttpClient: post/get(...)
alt use_session = true
MpesaHttpClient->>Session: request(method, url, timeout)
else use_session = false
MpesaHttpClient->>Requests: request(method, url, timeout)
end
MpesaHttpClient->>Tenacity: apply retry policy
Tenacity->>MpesaHttpClient: return response / raise on final attempt
alt Response 2xx
MpesaHttpClient-->>MpesaClient: parsed JSON
MpesaClient-->>Caller: parsed JSON
else Response non-2xx
MpesaHttpClient->>MpesaHttpClient: handle_request_error(response)
MpesaHttpClient-->>MpesaClient: raise MpesaApiException
MpesaClient-->>Caller: error
end
opt Retries exhausted or transport error
MpesaHttpClient->>MpesaHttpClient: handle_retry_exception(retry_state)
MpesaHttpClient-->>MpesaClient: raise MpesaApiException (REQUEST_TIMEOUT / CONNECTION_ERROR / REQUEST_FAILED)
MpesaClient-->>Caller: error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧹 Recent nitpick comments
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🔇 Additional comments (12)
✏️ Tip: You can disable this entire section by setting 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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (8)
pyproject.toml (1)
48-48: Consider allowing tenacity 9.x (latest fixes, same APIs used here).If compatible in CI, prefer >=9.1.2,<10.0.0.
Apply this diff:
- "tenacity>=8.2.3,<9.0.0" + "tenacity>=9.1.2,<10.0.0"Based on learnings
mpesakit/http_client/mpesa_http_client.py (2)
117-121: Make URL joining robust.Concatenation can double/miss slashes. Use urllib.parse.urljoin.
Apply these diffs:
+from urllib.parse import urljoin- full_url = f"{self.base_url}{url}" + full_url = urljoin(self.base_url + "/", url)And the same change in GET.
Also applies to: 154-159
23-44: Avoid double JSON parsing and broaden error message extraction.Only parse JSON on error, and fall back across common keys.
Apply this diff:
- try: - response_data = response.json() - except ValueError: - response_data = {"errorMessage": response.text.strip() or ""} - - if not response.ok: - error_message = response_data.get("errorMessage", "") + if not response.ok: + try: + response_data = response.json() + except ValueError: + response_data = {} + error_message = ( + response_data.get("errorMessage") + or response_data.get("message") + or response_data.get("error") + or (response.text.strip() if hasattr(response, "text") else "") + ) raise MpesaApiException( MpesaError( error_code=f"HTTP_{response.status_code}", error_message=error_message, status_code=response.status_code, raw_response=response_data, ) )mpesakit/mpesa_client.py (2)
23-28: Document the new use_session parameter.Add param doc so users discover session behavior and benefits.
Example:
def __init__(..., use_session: bool = False) -> None: """Initialize the MpesaClient with all service facades. Args: consumer_key: ... consumer_secret: ... environment: 'sandbox' or 'production'. use_session: Use a persistent requests.Session for connection pooling. """
27-27: Expose a close() to manage underlying HTTP session.Forward http_client.close() so apps can cleanly release resources.
Add:
def close(self) -> None: if hasattr(self.http_client, "close"): self.http_client.close()Optionally implement context manager to auto-close.
tests/unit/http_client/test_mpesa_http_client.py (3)
88-99: Align with “non-retryable” intent for RequestException and assert no retry.Capture the mock to assert a single call.
Apply this diff:
- with patch(patch_target, - side_effect=requests.RequestException("boom"), - ): - with pytest.raises(MpesaApiException) as exc: - client.post("/fail", json={}, headers={}) - - assert exc.value.error.error_code == "REQUEST_FAILED" + with patch(patch_target) as mock_post: + mock_post.side_effect = requests.RequestException("boom") + with pytest.raises(MpesaApiException) as exc: + client.post("/fail", json={}, headers={}) + assert exc.value.error.error_code == "REQUEST_FAILED" + mock_post.assert_called_once()Note: This assumes retries exclude RequestException as suggested in the client.
193-205: Do the same for GET: RequestException should not be retried.Assert only one attempt is made.
Apply this diff:
- with patch(patch_target, - side_effect=requests.RequestException("boom"), - ): - with pytest.raises(MpesaApiException) as exc: - - client.get("/fail") - - assert exc.value.error.error_code == "REQUEST_FAILED" + with patch(patch_target) as mock_get: + mock_get.side_effect = requests.RequestException("boom") + with pytest.raises(MpesaApiException) as exc: + client.get("/fail") + assert exc.value.error.error_code == "REQUEST_FAILED" + mock_get.assert_called_once()
39-52: Optional: Add a test for JSON decode error on success (ok=True).Covers the new JSON_DECODE_ERROR behavior on successful non-JSON responses.
Add:
def test_post_json_decode_error_on_success(client): patch_target = get_patch_target(client, "post") with patch(patch_target) as mock_post: mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 mock_response.json.side_effect = ValueError("invalid json") mock_response.text = "<html>not json</html>" mock_post.return_value = mock_response with pytest.raises(MpesaApiException) as exc: client.post("/ok-but-not-json", json={}, headers={}) assert exc.value.error.error_code == "JSON_DECODE_ERROR" def test_get_json_decode_error_on_success(client): patch_target = get_patch_target(client, "get") with patch(patch_target) as mock_get: mock_response = Mock() mock_response.ok = True mock_response.status_code = 200 mock_response.json.side_effect = ValueError("invalid json") mock_response.text = "<html>not json</html>" mock_get.return_value = mock_response with pytest.raises(MpesaApiException) as exc: client.get("/ok-but-not-json") assert exc.value.error.error_code == "JSON_DECODE_ERROR"Also applies to: 144-157
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
mpesakit/http_client/mpesa_http_client.py(2 hunks)mpesakit/mpesa_client.py(1 hunks)pyproject.toml(1 hunks)tests/unit/http_client/test_mpesa_http_client.py(8 hunks)
Don’t call logging.basicConfig in a library module. This globally alters app logging. Remove basicConfig; keep module logger only. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…l retryable and Public API implementations)
|
@coderabbitai review . |
|
✅ Actions performedReview triggered.
|
Migrating development branch
dev => developSummary by CodeRabbit
New Features
Refactor
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.