malipo 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
malipo/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .client import Malipo
2
+ from .errors import MalipoError
3
+
4
+ __all__ = ["Malipo", "MalipoError"]
malipo/client.py ADDED
@@ -0,0 +1,111 @@
1
+ import json
2
+ import hmac
3
+ import hashlib
4
+ import requests
5
+ from typing import Optional, Any, Dict
6
+ from .types import (
7
+ MalipoEnvironment,
8
+ MalipoTransaction,
9
+ MalipoBalance,
10
+ ChargeCreateParams,
11
+ MalipoEvent
12
+ )
13
+ from .errors import MalipoError
14
+
15
+ class Malipo:
16
+ def __init__(self, api_key: str, environment: Optional[MalipoEnvironment] = None, base_url: Optional[str] = None):
17
+ if not api_key:
18
+ raise ValueError("Malipo API Key is required.")
19
+
20
+ self.api_key = api_key
21
+
22
+ # Infer environment from API Key if not explicitly provided
23
+ if environment:
24
+ self.environment = environment
25
+ else:
26
+ self.environment = "live" if api_key.startswith("sk_live_") else "sandbox"
27
+
28
+ self.base_url = base_url or "https://lcwadpidhwptpzriqnjd.supabase.co/functions/v1"
29
+
30
+ # Initialize resources
31
+ self.charges = ChargesResource(self)
32
+ self.transactions = TransactionsResource(self)
33
+ self.balance = BalanceResource(self)
34
+ self.webhooks = WebhooksResource()
35
+
36
+ def request(self, method: str, endpoint: str, params: Optional[Dict] = None, headers: Optional[Dict] = None):
37
+ url = f"{self.base_url}{endpoint}"
38
+
39
+ default_headers = {
40
+ "Authorization": f"Bearer {self.api_key}",
41
+ "Content-Type": "application/json",
42
+ "X-Client-Info": "malipo-python/1.0.0"
43
+ }
44
+ if headers:
45
+ default_headers.update(headers)
46
+
47
+ try:
48
+ response = requests.request(
49
+ method=method,
50
+ url=url,
51
+ json=params if method == "POST" else None,
52
+ params=params if method == "GET" else None,
53
+ headers=default_headers
54
+ )
55
+
56
+ data = response.json()
57
+
58
+ if not response.ok:
59
+ error_data = data.get("error", {})
60
+ raise MalipoError(
61
+ message=error_data.get("message", "An unexpected error occurred"),
62
+ status_code=response.status_code,
63
+ code=error_data.get("code"),
64
+ details=error_data
65
+ )
66
+
67
+ return data
68
+
69
+ except requests.exceptions.RequestException as e:
70
+ raise MalipoError(f"Network error: {str(e)}")
71
+
72
+ class ChargesResource:
73
+ def __init__(self, client: Malipo):
74
+ self.client = client
75
+
76
+ def create(self, params: ChargeCreateParams, idempotency_key: Optional[str] = None) -> MalipoTransaction:
77
+ headers = {}
78
+ if idempotency_key:
79
+ headers["Idempotency-Key"] = idempotency_key
80
+ return self.client.request("POST", "/charge", params, headers)
81
+
82
+ class TransactionsResource:
83
+ def __init__(self, client: Malipo):
84
+ self.client = client
85
+
86
+ def retrieve(self, transaction_id: str) -> MalipoTransaction:
87
+ return self.client.request("GET", "/transaction-status", {"id": transaction_id})
88
+
89
+ class BalanceResource:
90
+ def __init__(self, client: Malipo):
91
+ self.client = client
92
+
93
+ def retrieve(self) -> MalipoBalance:
94
+ endpoint = "/live-balance" if self.client.environment == "live" else "/sandbox-balance"
95
+ return self.client.request("GET", endpoint)
96
+
97
+ class WebhooksResource:
98
+ def construct_event(self, payload: str, signature: str, secret: str) -> MalipoEvent:
99
+ if not payload or not signature or not secret:
100
+ raise ValueError("Missing payload, signature, or secret for webhook verification.")
101
+
102
+ expected_signature = hmac.new(
103
+ secret.encode("utf-8"),
104
+ payload.encode("utf-8"),
105
+ hashlib.sha256
106
+ ).hexdigest()
107
+
108
+ if not hmac.compare_digest(expected_signature, signature):
109
+ raise MalipoError("Invalid webhook signature.")
110
+
111
+ return json.loads(payload)
malipo/errors.py ADDED
@@ -0,0 +1,11 @@
1
+ class MalipoError(Exception):
2
+ """Base error for all Malipo SDK errors"""
3
+ def __init__(self, message: str, status_code: int = None, code: str = None, details: dict = None):
4
+ super().__init__(message)
5
+ self.message = message
6
+ self.status_code = status_code
7
+ self.code = code
8
+ self.details = details
9
+
10
+ def __str__(self):
11
+ return f"{self.message} (status: {self.status_code}, code: {self.code})"
malipo/types.py ADDED
@@ -0,0 +1,51 @@
1
+ from typing import TypedDict, List, Optional, Any, Literal
2
+
3
+ MalipoNetwork = Literal["VODACOM_MPESA", "AIRTEL_MONEY", "ORANGE_MONEY"]
4
+ MalipoTransactionStatus = Literal["pending", "succeeded", "failed", "expired"]
5
+ MalipoEnvironment = Literal["sandbox", "live"]
6
+
7
+ class MalipoConfig(TypedDict):
8
+ api_key: str
9
+ environment: Optional[MalipoEnvironment]
10
+ base_url: Optional[str]
11
+
12
+ class ChargeCreateParams(TypedDict, total=False):
13
+ amount: float
14
+ currency: str
15
+ phone: str
16
+ network: MalipoNetwork
17
+ description: Optional[str]
18
+ metadata: Optional[dict]
19
+
20
+ class MalipoTransaction(TypedDict):
21
+ id: str
22
+ object: Literal["transaction"]
23
+ amount: float
24
+ currency: str
25
+ status: MalipoTransactionStatus
26
+ phone: str
27
+ network: MalipoNetwork
28
+ environment: MalipoEnvironment
29
+ metadata: dict
30
+ created_at: str
31
+ updated_at: str
32
+ failure_reason: Optional[str]
33
+ failure_code: Optional[str]
34
+
35
+ class MalipoBalanceAmount(TypedDict):
36
+ amount: float
37
+ currency: str
38
+
39
+ class MalipoBalance(TypedDict):
40
+ object: Literal["balance"]
41
+ available: List[MalipoBalanceAmount]
42
+ pending: List[MalipoBalanceAmount]
43
+ updated_at: str
44
+
45
+ class MalipoEvent(TypedDict):
46
+ id: str
47
+ object: Literal["event"]
48
+ type: str
49
+ environment: MalipoEnvironment
50
+ data: dict # contains object: MalipoTransaction
51
+ created_at: str
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: malipo
3
+ Version: 1.0.0
4
+ Summary: The official Python library for the Malipo Payment Gateway
5
+ Author-email: Malipo Dev Team <dev@malipo.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.8
8
+ Requires-Dist: requests>=2.25.0
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest>=7.0.0; extra == 'test'
11
+ Requires-Dist: responses>=0.18.0; extra == 'test'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Malipo Python SDK
15
+
16
+ The official Python library for the Malipo Payment Gateway. Securely accept Mobile Money payments (Vodacom MPesa, Orange Money, Airtel Money) in the DRC.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install malipo
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```python
27
+ from malipo import Malipo
28
+
29
+ malipo = Malipo(api_key='sk_test_your_api_key')
30
+
31
+ # Create a charge
32
+ try:
33
+ charge = malipo.charges.create({
34
+ "amount": 10,
35
+ "currency": "USD",
36
+ "phone": "243810000000",
37
+ "network": "VODACOM_MPESA",
38
+ "description": "Order #123"
39
+ }, idempotency_key='unique_order_id_123')
40
+
41
+ print(f"Charge initiated: {charge['id']}")
42
+ print(f"Status: {charge['status']}") # 'pending'
43
+ except Exception as e:
44
+ print(f"Charge failed: {e}")
45
+ ```
46
+
47
+ ## Features
48
+
49
+ ### 🔐 Idempotency
50
+ Protect against duplicate charges by providing an `idempotency_key` in the options.
51
+
52
+ ### 🔄 Environment Detection
53
+ The SDK automatically switches between `sandbox` and `live` environments based on your API key prefix (`sk_test_` vs `sk_live_`).
54
+
55
+ ### 📊 Balance Check
56
+ Check your available and pending balances for your current environment.
57
+
58
+ ```python
59
+ balance = malipo.balance.retrieve()
60
+ print(f"Available USD: {balance['available'][0]['amount']}")
61
+ ```
62
+
63
+ ## Webhooks
64
+
65
+ ```python
66
+ from flask import Flask, request
67
+ from malipo import Malipo
68
+
69
+ app = Flask(__name__)
70
+ malipo = Malipo(api_key='...')
71
+
72
+ @app.route('/webhooks/malipo', methods=['POST'])
73
+ def handle_webhook():
74
+ signature = request.headers.get('X-Webhook-Signature')
75
+ payload = request.get_data(as_text=True)
76
+
77
+ try:
78
+ event = malipo.webhooks.construct_event(
79
+ payload=payload,
80
+ signature=signature,
81
+ secret='whsec_your_secret'
82
+ )
83
+
84
+ if event['type'] == 'charge.succeeded':
85
+ charge = event['data']['object']
86
+ print(f"Payment successful: {charge['id']}")
87
+
88
+ return "OK", 200
89
+ except Exception as e:
90
+ return str(e), 400
91
+ ```
92
+
93
+ ## License
94
+ MIT
@@ -0,0 +1,7 @@
1
+ malipo/__init__.py,sha256=M2cWLNI3H76rY5UzZRjJ7L3vrf27PIt5SQEusek9fPw,96
2
+ malipo/client.py,sha256=cSJjkcW6jmFZqRzdD965xxMIbUakLabkVCyYi-BMgP4,3949
3
+ malipo/errors.py,sha256=rd1yvMrw9BlMMlwRZ4BOexiAM9LH3R5CTswsS7RA5cQ,445
4
+ malipo/types.py,sha256=JV-Gxz7nLuoCQvGkyu-cVTcDcblYbQcy1gFYNnSfYGE,1360
5
+ malipo-1.0.0.dist-info/METADATA,sha256=nSpPN-3vqPry9wAYw2bUBRRsBMhGk2yZDd5MkVHj8Qk,2397
6
+ malipo-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ malipo-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any