diff --git a/docs/src/components/MpesaKit.module.css b/docs/src/components/MpesaKit.module.css index 9748464..f71cb65 100644 --- a/docs/src/components/MpesaKit.module.css +++ b/docs/src/components/MpesaKit.module.css @@ -4,9 +4,8 @@ --mpesa-green: #00D13A; --mpesa-dark-green: #00B032; --mpesa-light-green: #4AE668; - --dark-bg: #0a0a0a; - --card-bg: rgba(255, 255, 255, 0.05); - --text-primary: #ffffff; + --card-bg: rgba(0, 0, 0, 0.05); + --text-primary: #1a1a1a; --text-secondary: #a0a0a0; --gradient-primary: linear-gradient(135deg, var(--mpesa-green) 0%, var(--mpesa-light-green) 100%); --gradient-dark: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); @@ -140,10 +139,10 @@ justify-content: center; flex-direction: column; } - .btn { padding: 0.875rem 1.5rem; - border: none; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 50px; border-radius: 50px; font-weight: 600; text-decoration: none; @@ -259,6 +258,7 @@ overflow-x: auto; -webkit-overflow-scrolling: touch; white-space: nowrap; + color: #f8f8f8; } .codeLine { @@ -610,11 +610,11 @@ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 2rem; } - .featureCard { background: var(--card-bg); border-radius: 20px; padding: 2.5rem; + border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; position: relative; @@ -738,15 +738,13 @@ font-size: 1.2rem; flex-shrink: 0; } - -.securityVisual { - /* Styles handled inline in component */ -} +/* Security visual styles are handled inline in the component */ /* API Status Section */ .apiStatus { padding: 6rem 5%; - background: rgba(0, 0, 0, 0.3); + background: rgba(0, 209, 58, 0.02); + color: var(--text-primary); } .statusGrid { diff --git a/mpesakit/http_client/mpesa_http_client.py b/mpesakit/http_client/mpesa_http_client.py index a5d755b..f99b389 100644 --- a/mpesakit/http_client/mpesa_http_client.py +++ b/mpesakit/http_client/mpesa_http_client.py @@ -4,7 +4,7 @@ """ from typing import Dict, Any, Optional -import requests +import httpx from mpesakit.errors import MpesaError, MpesaApiException from .http_client import HttpClient @@ -54,14 +54,15 @@ def post( """ try: full_url = f"{self.base_url}{url}" - response = requests.post(full_url, json=json, headers=headers, timeout=10) + with httpx.Client(timeout=10) as client: + response = client.post(full_url, json=json, headers=headers) try: response_data = response.json() except ValueError: response_data = {"errorMessage": response.text.strip() or ""} - if not response.ok: + if response.status_code >= 400: error_message = response_data.get("errorMessage", "") raise MpesaApiException( MpesaError( @@ -74,7 +75,7 @@ def post( return response_data - except requests.Timeout: + except httpx.TimeoutException: raise MpesaApiException( MpesaError( error_code="REQUEST_TIMEOUT", @@ -82,15 +83,7 @@ def post( status_code=None, ) ) - except requests.ConnectionError: - raise MpesaApiException( - MpesaError( - error_code="CONNECTION_ERROR", - error_message="Failed to connect to Mpesa API. Check network or URL.", - status_code=None, - ) - ) - except requests.RequestException as e: + except httpx.RequestError as e: raise MpesaApiException( MpesaError( error_code="REQUEST_FAILED", @@ -124,16 +117,15 @@ def get( headers = {} full_url = f"{self.base_url}{url}" - response = requests.get( - full_url, params=params, headers=headers, timeout=10 - ) # Add timeout + with httpx.Client(timeout=10) as client: + response = client.get(full_url, params=params, headers=headers) try: response_data = response.json() except ValueError: response_data = {"errorMessage": response.text.strip() or ""} - if not response.ok: + if not response.is_success: error_message = response_data.get("errorMessage", "") raise MpesaApiException( MpesaError( @@ -146,7 +138,7 @@ def get( return response_data - except requests.Timeout: + except httpx.TimeoutException: raise MpesaApiException( MpesaError( error_code="REQUEST_TIMEOUT", @@ -154,15 +146,7 @@ def get( status_code=None, ) ) - except requests.ConnectionError: - raise MpesaApiException( - MpesaError( - error_code="CONNECTION_ERROR", - error_message="Failed to connect to Mpesa API. Check network or URL.", - status_code=None, - ) - ) - except requests.RequestException as e: + except httpx.RequestError as e: raise MpesaApiException( MpesaError( error_code="REQUEST_FAILED", diff --git a/tests/integration/mpesa_express/test_stk_push_e2e.py b/tests/integration/mpesa_express/test_stk_push_e2e.py index 70531c1..6a65869 100644 --- a/tests/integration/mpesa_express/test_stk_push_e2e.py +++ b/tests/integration/mpesa_express/test_stk_push_e2e.py @@ -7,7 +7,7 @@ import os import time import pytest -import requests +import httpx from threading import Thread from dotenv import load_dotenv @@ -82,7 +82,8 @@ def test_stk_push_full_e2e_with_query(stk_service, fastapi_server, ngrok_tunnel) print("🔗 Starting E2E Test: STK Push, Callback, and Query") # 1. Clear previous callbacks callback_base_url = f"{ngrok_tunnel}/mpesa/callback" - requests.post(f"{callback_base_url}/clear") + with httpx.Client() as client: + client.post(f"{callback_base_url}/clear") callback_url = f"{ngrok_tunnel}/mpesa/callback" print(f"📨 Using callback URL: {callback_url}") @@ -111,18 +112,19 @@ def test_stk_push_full_e2e_with_query(stk_service, fastapi_server, ngrok_tunnel) print("⏳ Waiting up to 30 seconds for callback...") callback_received = False callback = None - for _ in range(30): - time.sleep(1) - r = requests.get(f"{callback_base_url}/latest", timeout=45) - if r.status_code == 200: - callback_received = True - callback_json = r.json()["parsed"] - callback = StkPushSimulateCallback.model_validate(callback_json) - body = callback.Body.stkCallback - print( - f"🎉 Callback received: ResultCode={body.ResultCode}, Desc={body.ResultDesc}" - ) - break + with httpx.Client() as client: + for _ in range(30): + time.sleep(1) + r = client.get(f"{callback_base_url}/latest", timeout=45) + if r.status_code == 200: + callback_received = True + callback_json = r.json()["parsed"] + callback = StkPushSimulateCallback.model_validate(callback_json) + body = callback.Body.stkCallback + print( + f"🎉 Callback received: ResultCode={body.ResultCode}, Desc={body.ResultDesc}" + ) + break if not callback_received: print( diff --git a/tests/unit/http_client/test_mpesa_http_client.py b/tests/unit/http_client/test_mpesa_http_client.py index c2a38e4..308e952 100644 --- a/tests/unit/http_client/test_mpesa_http_client.py +++ b/tests/unit/http_client/test_mpesa_http_client.py @@ -4,7 +4,7 @@ HTTP POST and GET request handling, and error handling for various scenarios. """ -import requests +import httpx import pytest from unittest.mock import Mock, patch from mpesakit.http_client.mpesa_http_client import MpesaHttpClient @@ -32,9 +32,9 @@ def test_base_url_production(): def test_post_success(client): """Test successful POST request returns expected JSON.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.post") as mock_post: mock_response = Mock() - mock_response.ok = True + mock_response.status_code = 200 mock_response.json.return_value = {"foo": "bar"} mock_post.return_value = mock_response @@ -45,9 +45,8 @@ def test_post_success(client): def test_post_http_error(client): """Test POST request returns MpesaApiException on HTTP error.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.post") as mock_post: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 400 mock_response.json.return_value = {"errorMessage": "Bad Request"} mock_post.return_value = mock_response @@ -60,9 +59,8 @@ def test_post_http_error(client): def test_post_json_decode_error(client): """Test POST request handles JSON decode error gracefully.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.post") as mock_post: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.post") as mock_post: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 500 mock_response.json.side_effect = ValueError() mock_response.text = "Internal Server Error" @@ -77,8 +75,8 @@ def test_post_json_decode_error(client): def test_post_request_exception(client): """Test POST request raises MpesaApiException on generic exception.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.post", - side_effect=requests.RequestException("boom"), + "mpesakit.http_client.mpesa_http_client.httpx.Client.post", + side_effect=httpx.RequestError("boom"), ): with pytest.raises(MpesaApiException) as exc: client.post("/fail", json={}, headers={}) @@ -88,8 +86,8 @@ def test_post_request_exception(client): def test_post_timeout(client): """Test POST request raises MpesaApiException on timeout.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.post", - side_effect=requests.Timeout, + "mpesakit.http_client.mpesa_http_client.httpx.Client.post", + side_effect=httpx.TimeoutException, ): with pytest.raises(MpesaApiException) as exc: client.post("/timeout", json={}, headers={}) @@ -99,8 +97,8 @@ def test_post_timeout(client): def test_post_connection_error(client): """Test POST request raises MpesaApiException on connection error.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.post", - side_effect=requests.ConnectionError, + "mpesakit.http_client.mpesa_http_client.httpx.Client.post", + side_effect=httpx.ConnectError, ): with pytest.raises(MpesaApiException) as exc: client.post("/conn", json={}, headers={}) @@ -109,7 +107,7 @@ def test_post_connection_error(client): def test_get_success(client): """Test successful GET request returns expected JSON.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.get") as mock_get: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"foo": "bar"} @@ -122,9 +120,8 @@ def test_get_success(client): def test_get_http_error(client): """Test GET request returns MpesaApiException on HTTP error.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.get") as mock_get: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 404 mock_response.json.return_value = {"errorMessage": "Not Found"} mock_get.return_value = mock_response @@ -137,9 +134,8 @@ def test_get_http_error(client): def test_get_json_decode_error(client): """Test GET request handles JSON decode error gracefully.""" - with patch("mpesakit.http_client.mpesa_http_client.requests.get") as mock_get: + with patch("mpesakit.http_client.mpesa_http_client.httpx.Client.get") as mock_get: mock_response = Mock() - mock_response.ok = False mock_response.status_code = 500 mock_response.json.side_effect = ValueError() mock_response.text = "Internal Server Error" @@ -154,8 +150,8 @@ def test_get_json_decode_error(client): def test_get_request_exception(client): """Test GET request raises MpesaApiException on generic exception.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.get", - side_effect=requests.RequestException("boom"), + "mpesakit.http_client.mpesa_http_client.httpx.Client.get", + side_effect=httpx.RequestError("boom"), ): with pytest.raises(MpesaApiException) as exc: client.get("/fail") @@ -165,8 +161,8 @@ def test_get_request_exception(client): def test_get_timeout(client): """Test GET request raises MpesaApiException on timeout.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.get", - side_effect=requests.Timeout, + "mpesakit.http_client.mpesa_http_client.httpx.Client.get", + side_effect=httpx.TimeoutException, ): with pytest.raises(MpesaApiException) as exc: client.get("/timeout") @@ -176,8 +172,8 @@ def test_get_timeout(client): def test_get_connection_error(client): """Test GET request raises MpesaApiException on connection error.""" with patch( - "mpesakit.http_client.mpesa_http_client.requests.get", - side_effect=requests.ConnectionError, + "mpesakit.http_client.mpesa_http_client.httpx.Client.get", + side_effect=httpx.ConnectError, ): with pytest.raises(MpesaApiException) as exc: client.get("/conn")