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 +4 -0
- malipo/client.py +111 -0
- malipo/errors.py +11 -0
- malipo/types.py +51 -0
- malipo-1.0.0.dist-info/METADATA +94 -0
- malipo-1.0.0.dist-info/RECORD +7 -0
- malipo-1.0.0.dist-info/WHEEL +4 -0
malipo/__init__.py
ADDED
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,,
|