bml-connect-python 1.1.2__tar.gz → 1.2.0__tar.gz
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.
- {bml_connect_python-1.1.2 → bml_connect_python-1.2.0}/PKG-INFO +113 -81
- {bml_connect_python-1.1.2 → bml_connect_python-1.2.0}/README.md +112 -80
- {bml_connect_python-1.1.2 → bml_connect_python-1.2.0}/pyproject.toml +1 -1
- {bml_connect_python-1.1.2 → bml_connect_python-1.2.0}/src/bml_connect/client.py +93 -35
- {bml_connect_python-1.1.2 → bml_connect_python-1.2.0}/LICENSE +0 -0
- {bml_connect_python-1.1.2 → bml_connect_python-1.2.0}/src/bml_connect/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bml-connect-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Python SDK for Bank of Maldives Connect API with synchronous and asynchronous support
|
|
5
5
|
Home-page: https://github.com/quillfires/bml-connect-python
|
|
6
6
|
License: MIT
|
|
@@ -34,8 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
[](https://pypi.org/project/bml-connect-python/)
|
|
35
35
|
[](https://opensource.org/licenses/MIT)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
[](https://views.whatilearened.today/views/github/quillfires/bml-connect-python.svg) [](https://github.com/quillfires/bml-connect-python/network) [](https://github.com/quillfires/bml-connect-python/stargazers) [](https://pypi.python.org/pypi/bml-connect-python/) [](https://github.com/quillfires/bml-connect-python/issues) [](https://github.com/quillfires/bml-connect-python/issues)
|
|
37
|
+
[](https://views.whatilearened.today/views/github/quillfires/bml-connect-python.svg) [](https://github.com/quillfires/bml-connect-python/network) [](https://github.com/quillfires/bml-connect-python/stargazers) [](https://pypi.python.org/pypi/bml-connect-python/) [](https://github.com/quillfires/bml-connect-python/issues) [](https://github.com/quillfires/bml-connect-python/issues)
|
|
39
38
|
|
|
40
39
|
Python SDK for Bank of Maldives Connect API with synchronous and asynchronous support.
|
|
41
40
|
Compatible with all Python frameworks including Django, Flask, FastAPI, and Sanic.
|
|
@@ -43,10 +42,11 @@ Compatible with all Python frameworks including Django, Flask, FastAPI, and Sani
|
|
|
43
42
|
## Features
|
|
44
43
|
|
|
45
44
|
- **🔄 Sync/Async Support**: Choose your preferred programming style
|
|
46
|
-
- **🎯 Full API Coverage**:
|
|
45
|
+
- **🎯 Full API Coverage**: Create, retrieve, cancel transactions; webhook signature verification
|
|
47
46
|
- **📝 Type Annotations**: Full type hint support for better development experience
|
|
48
47
|
- **🛡️ Error Handling**: Comprehensive error hierarchy for easy debugging
|
|
49
48
|
- **🚀 Framework Agnostic**: Works with any Python web framework
|
|
49
|
+
- **🔒 Context Manager Support**: Automatic resource cleanup with `with`/`async with`
|
|
50
50
|
- **📄 MIT Licensed**: Open source and free to use
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
@@ -62,13 +62,12 @@ pip install bml-connect-python
|
|
|
62
62
|
```python
|
|
63
63
|
from bml_connect import BMLConnect, Environment
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
# Use as a context manager for automatic cleanup
|
|
66
|
+
with BMLConnect(
|
|
66
67
|
api_key="your_api_key",
|
|
67
68
|
app_id="your_app_id",
|
|
68
69
|
environment=Environment.SANDBOX
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
try:
|
|
70
|
+
) as client:
|
|
72
71
|
transaction = client.transactions.create_transaction({
|
|
73
72
|
"amount": 1500, # 15.00 MVR
|
|
74
73
|
"currency": "MVR",
|
|
@@ -79,10 +78,6 @@ try:
|
|
|
79
78
|
})
|
|
80
79
|
print(f"Transaction ID: {transaction.transaction_id}")
|
|
81
80
|
print(f"Payment URL: {transaction.url}")
|
|
82
|
-
except Exception as e:
|
|
83
|
-
print(f"Error: {e}")
|
|
84
|
-
finally:
|
|
85
|
-
client.close()
|
|
86
81
|
```
|
|
87
82
|
|
|
88
83
|
### Asynchronous Usage
|
|
@@ -92,14 +87,12 @@ import asyncio
|
|
|
92
87
|
from bml_connect import BMLConnect, Environment
|
|
93
88
|
|
|
94
89
|
async def main():
|
|
95
|
-
|
|
90
|
+
async with BMLConnect(
|
|
96
91
|
api_key="your_api_key",
|
|
97
92
|
app_id="your_app_id",
|
|
98
93
|
environment=Environment.SANDBOX,
|
|
99
94
|
async_mode=True
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
try:
|
|
95
|
+
) as client:
|
|
103
96
|
transaction = await client.transactions.create_transaction({
|
|
104
97
|
"amount": 2000,
|
|
105
98
|
"currency": "MVR",
|
|
@@ -107,8 +100,6 @@ async def main():
|
|
|
107
100
|
"redirectUrl": "https://yourstore.com/success"
|
|
108
101
|
})
|
|
109
102
|
print(f"Transaction ID: {transaction.transaction_id}")
|
|
110
|
-
finally:
|
|
111
|
-
await client.aclose()
|
|
112
103
|
|
|
113
104
|
asyncio.run(main())
|
|
114
105
|
```
|
|
@@ -128,7 +119,7 @@ client = BMLConnect(api_key="your_api_key", app_id="your_app_id")
|
|
|
128
119
|
def webhook():
|
|
129
120
|
payload = request.get_json()
|
|
130
121
|
signature = payload.get('signature')
|
|
131
|
-
|
|
122
|
+
|
|
132
123
|
if client.verify_webhook_signature(payload, signature):
|
|
133
124
|
# Process webhook
|
|
134
125
|
return jsonify({"status": "success"}), 200
|
|
@@ -149,7 +140,7 @@ client = BMLConnect(api_key="your_api_key", app_id="your_app_id")
|
|
|
149
140
|
async def handle_webhook(request: Request):
|
|
150
141
|
payload = await request.json()
|
|
151
142
|
signature = payload.get("signature")
|
|
152
|
-
|
|
143
|
+
|
|
153
144
|
if client.verify_webhook_signature(payload, signature):
|
|
154
145
|
return {"status": "success"}
|
|
155
146
|
else:
|
|
@@ -169,7 +160,7 @@ client = BMLConnect(api_key="your_api_key", app_id="your_app_id")
|
|
|
169
160
|
async def webhook(request):
|
|
170
161
|
payload = request.json
|
|
171
162
|
signature = payload.get('signature')
|
|
172
|
-
|
|
163
|
+
|
|
173
164
|
if client.verify_webhook_signature(payload, signature):
|
|
174
165
|
return response.json({"status": "success"})
|
|
175
166
|
else:
|
|
@@ -178,25 +169,59 @@ async def webhook(request):
|
|
|
178
169
|
|
|
179
170
|
## API Reference
|
|
180
171
|
|
|
172
|
+
### `BMLConnect(api_key, app_id, environment, async_mode, timeout)`
|
|
173
|
+
|
|
174
|
+
Main entry point for the SDK.
|
|
175
|
+
|
|
176
|
+
| Parameter | Type | Default | Description |
|
|
177
|
+
| ------------- | ---------------------- | ------------ | ------------------------------------------------- |
|
|
178
|
+
| `api_key` | `str` | required | Your API key from the BML merchant portal |
|
|
179
|
+
| `app_id` | `str` | required | Your application ID from the BML merchant portal |
|
|
180
|
+
| `environment` | `Environment` or `str` | `PRODUCTION` | `Environment.SANDBOX` or `Environment.PRODUCTION` |
|
|
181
|
+
| `async_mode` | `bool` | `False` | Set `True` to use async methods |
|
|
182
|
+
| `timeout` | `int` | `30` | Request timeout in seconds |
|
|
183
|
+
|
|
184
|
+
### Transaction Methods
|
|
185
|
+
|
|
186
|
+
| Method | Sync | Async | Description |
|
|
187
|
+
| -------------------------- | ---- | ----- | --------------------------------------- |
|
|
188
|
+
| `create_transaction(data)` | ✅ | ✅ | Create a new payment transaction |
|
|
189
|
+
| `get_transaction(id)` | ✅ | ✅ | Retrieve a transaction by ID |
|
|
190
|
+
| `cancel_transaction(id)` | ✅ | ✅ | Cancel a transaction by ID |
|
|
191
|
+
| `list_transactions(...)` | ✅ | ✅ | List transactions with optional filters |
|
|
192
|
+
|
|
193
|
+
### `list_transactions` Parameters
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
client.transactions.list_transactions(
|
|
197
|
+
page=1,
|
|
198
|
+
per_page=20,
|
|
199
|
+
state="CONFIRMED", # Filter by TransactionState value
|
|
200
|
+
provider="alipay", # Filter by provider
|
|
201
|
+
start_date="2026-01-01", # Filter from date
|
|
202
|
+
end_date="2026-02-01", # Filter to date
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
181
206
|
### Core Classes
|
|
182
207
|
|
|
183
208
|
- **`BMLConnect`**: Main entry point for the SDK
|
|
184
|
-
- **`Transaction`**:
|
|
185
|
-
- **`QRCode`**: QR code details
|
|
186
|
-
- **`PaginatedResponse`**:
|
|
187
|
-
- **`Environment`**:
|
|
188
|
-
- **`SignMethod`**:
|
|
189
|
-
- **`TransactionState`**:
|
|
209
|
+
- **`Transaction`**: Typed transaction object returned by all transaction methods
|
|
210
|
+
- **`QRCode`**: QR code details attached to a transaction
|
|
211
|
+
- **`PaginatedResponse`**: Wraps paginated transaction list results
|
|
212
|
+
- **`Environment`**: `SANDBOX` or `PRODUCTION`
|
|
213
|
+
- **`SignMethod`**: `SHA1` (default) or `MD5`
|
|
214
|
+
- **`TransactionState`**: `CREATED`, `QR_CODE_GENERATED`, `CONFIRMED`, `CANCELLED`, `FAILED`, `EXPIRED`, `REFUND_REQUESTED`, `REFUNDED`
|
|
190
215
|
|
|
191
216
|
### Exception Hierarchy
|
|
192
217
|
|
|
193
218
|
```
|
|
194
219
|
BMLConnectError
|
|
195
|
-
├── AuthenticationError
|
|
196
|
-
├── ValidationError
|
|
197
|
-
├── NotFoundError
|
|
198
|
-
├── ServerError
|
|
199
|
-
└── RateLimitError
|
|
220
|
+
├── AuthenticationError (401)
|
|
221
|
+
├── ValidationError (400)
|
|
222
|
+
├── NotFoundError (404)
|
|
223
|
+
├── ServerError (5xx)
|
|
224
|
+
└── RateLimitError (429)
|
|
200
225
|
```
|
|
201
226
|
|
|
202
227
|
### Signature Utilities
|
|
@@ -204,10 +229,10 @@ BMLConnectError
|
|
|
204
229
|
```python
|
|
205
230
|
from bml_connect import SignatureUtils
|
|
206
231
|
|
|
207
|
-
# Generate signature
|
|
232
|
+
# Generate a signature manually
|
|
208
233
|
signature = SignatureUtils.generate_signature(data, api_key, method)
|
|
209
234
|
|
|
210
|
-
# Verify signature
|
|
235
|
+
# Verify a signature with constant-time comparison
|
|
211
236
|
is_valid = SignatureUtils.verify_signature(data, signature, api_key, method)
|
|
212
237
|
```
|
|
213
238
|
|
|
@@ -216,24 +241,31 @@ is_valid = SignatureUtils.verify_signature(data, signature, api_key, method)
|
|
|
216
241
|
### Transaction Management
|
|
217
242
|
|
|
218
243
|
```python
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
with BMLConnect(api_key="your_api_key", app_id="your_app_id") as client:
|
|
245
|
+
# Create
|
|
246
|
+
transaction = client.transactions.create_transaction({
|
|
247
|
+
"amount": 5000,
|
|
248
|
+
"currency": "MVR",
|
|
249
|
+
"provider": "alipay",
|
|
250
|
+
"redirectUrl": "https://yourstore.com/success",
|
|
251
|
+
"localId": "order_456"
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
# Retrieve
|
|
255
|
+
details = client.transactions.get_transaction(transaction.transaction_id)
|
|
256
|
+
print(f"State: {details.state}")
|
|
257
|
+
|
|
258
|
+
# Cancel
|
|
259
|
+
cancelled = client.transactions.cancel_transaction(transaction.transaction_id)
|
|
260
|
+
|
|
261
|
+
# List with filters
|
|
262
|
+
results = client.transactions.list_transactions(
|
|
263
|
+
page=1,
|
|
264
|
+
per_page=10,
|
|
265
|
+
state="CONFIRMED"
|
|
266
|
+
)
|
|
267
|
+
for t in results.items:
|
|
268
|
+
print(t.transaction_id, t.amount, t.state)
|
|
237
269
|
```
|
|
238
270
|
|
|
239
271
|
### Webhook Handling
|
|
@@ -242,39 +274,45 @@ transactions = client.transactions.list_transactions(
|
|
|
242
274
|
@app.route('/webhook', methods=['POST'])
|
|
243
275
|
def handle_webhook():
|
|
244
276
|
payload = request.get_json()
|
|
245
|
-
|
|
246
|
-
# Verify webhook signature
|
|
277
|
+
|
|
247
278
|
if not client.verify_webhook_signature(payload, payload.get('signature')):
|
|
248
279
|
return {"error": "Invalid signature"}, 403
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
pass
|
|
259
|
-
|
|
280
|
+
|
|
281
|
+
state = payload.get('state')
|
|
282
|
+
if state == 'CONFIRMED':
|
|
283
|
+
pass # fulfil the order
|
|
284
|
+
elif state == 'REFUND_REQUESTED':
|
|
285
|
+
pass # initiate refund flow
|
|
286
|
+
elif state == 'REFUNDED':
|
|
287
|
+
pass # mark order as refunded
|
|
288
|
+
|
|
260
289
|
return {"status": "success"}
|
|
261
290
|
```
|
|
262
291
|
|
|
292
|
+
### Custom Timeout
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
# For slow network environments or large payloads
|
|
296
|
+
client = BMLConnect(
|
|
297
|
+
api_key="your_api_key",
|
|
298
|
+
app_id="your_app_id",
|
|
299
|
+
timeout=60
|
|
300
|
+
)
|
|
301
|
+
```
|
|
302
|
+
|
|
263
303
|
## Requirements
|
|
264
304
|
|
|
265
|
-
- Python 3.
|
|
266
|
-
-
|
|
305
|
+
- Python 3.9+
|
|
306
|
+
- `requests`
|
|
307
|
+
- `aiohttp`
|
|
267
308
|
|
|
268
309
|
## Development
|
|
269
310
|
|
|
270
|
-
### Setup
|
|
311
|
+
### Setup
|
|
271
312
|
|
|
272
313
|
```bash
|
|
273
|
-
# Clone the repository
|
|
274
314
|
git clone https://github.com/quillfires/bml-connect-python.git
|
|
275
315
|
cd bml-connect-python
|
|
276
|
-
|
|
277
|
-
# Install in development mode
|
|
278
316
|
pip install -e .[dev]
|
|
279
317
|
```
|
|
280
318
|
|
|
@@ -287,13 +325,8 @@ pytest
|
|
|
287
325
|
### Code Quality
|
|
288
326
|
|
|
289
327
|
```bash
|
|
290
|
-
# Format code
|
|
291
328
|
black .
|
|
292
|
-
|
|
293
|
-
# Lint code
|
|
294
329
|
flake8 .
|
|
295
|
-
|
|
296
|
-
# Type checking
|
|
297
330
|
mypy .
|
|
298
331
|
```
|
|
299
332
|
|
|
@@ -312,7 +345,7 @@ bml-connect-python/
|
|
|
312
345
|
|
|
313
346
|
## Contributing
|
|
314
347
|
|
|
315
|
-
|
|
348
|
+
Contributions are welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
316
349
|
|
|
317
350
|
1. Fork the repository
|
|
318
351
|
2. Create a feature branch
|
|
@@ -323,7 +356,7 @@ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.
|
|
|
323
356
|
|
|
324
357
|
## License
|
|
325
358
|
|
|
326
|
-
|
|
359
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
327
360
|
|
|
328
361
|
## Support
|
|
329
362
|
|
|
@@ -333,14 +366,13 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
333
366
|
|
|
334
367
|
## Changelog
|
|
335
368
|
|
|
336
|
-
See [CHANGELOG.md](CHANGELOG.md) for a
|
|
369
|
+
See [CHANGELOG.md](https://github.com/quillfires/bml-connect-python/blob/main/CHANGELOG.md) for a full history of changes.
|
|
337
370
|
|
|
338
371
|
## Security
|
|
339
372
|
|
|
340
|
-
If you discover
|
|
373
|
+
If you discover a security issue, please email fayaz.quill@gmail.com instead of opening a public issue.
|
|
341
374
|
|
|
342
375
|
---
|
|
343
376
|
|
|
344
|
-
|
|
345
377
|
Made with ❤️ for the Maldivian developer community
|
|
346
378
|
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
[](https://pypi.org/project/bml-connect-python/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[](https://views.whatilearened.today/views/github/quillfires/bml-connect-python.svg) [](https://github.com/quillfires/bml-connect-python/network) [](https://github.com/quillfires/bml-connect-python/stargazers) [](https://pypi.python.org/pypi/bml-connect-python/) [](https://github.com/quillfires/bml-connect-python/issues) [](https://github.com/quillfires/bml-connect-python/issues)
|
|
7
|
+
[](https://views.whatilearened.today/views/github/quillfires/bml-connect-python.svg) [](https://github.com/quillfires/bml-connect-python/network) [](https://github.com/quillfires/bml-connect-python/stargazers) [](https://pypi.python.org/pypi/bml-connect-python/) [](https://github.com/quillfires/bml-connect-python/issues) [](https://github.com/quillfires/bml-connect-python/issues)
|
|
9
8
|
|
|
10
9
|
Python SDK for Bank of Maldives Connect API with synchronous and asynchronous support.
|
|
11
10
|
Compatible with all Python frameworks including Django, Flask, FastAPI, and Sanic.
|
|
@@ -13,10 +12,11 @@ Compatible with all Python frameworks including Django, Flask, FastAPI, and Sani
|
|
|
13
12
|
## Features
|
|
14
13
|
|
|
15
14
|
- **🔄 Sync/Async Support**: Choose your preferred programming style
|
|
16
|
-
- **🎯 Full API Coverage**:
|
|
15
|
+
- **🎯 Full API Coverage**: Create, retrieve, cancel transactions; webhook signature verification
|
|
17
16
|
- **📝 Type Annotations**: Full type hint support for better development experience
|
|
18
17
|
- **🛡️ Error Handling**: Comprehensive error hierarchy for easy debugging
|
|
19
18
|
- **🚀 Framework Agnostic**: Works with any Python web framework
|
|
19
|
+
- **🔒 Context Manager Support**: Automatic resource cleanup with `with`/`async with`
|
|
20
20
|
- **📄 MIT Licensed**: Open source and free to use
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
@@ -32,13 +32,12 @@ pip install bml-connect-python
|
|
|
32
32
|
```python
|
|
33
33
|
from bml_connect import BMLConnect, Environment
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Use as a context manager for automatic cleanup
|
|
36
|
+
with BMLConnect(
|
|
36
37
|
api_key="your_api_key",
|
|
37
38
|
app_id="your_app_id",
|
|
38
39
|
environment=Environment.SANDBOX
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
try:
|
|
40
|
+
) as client:
|
|
42
41
|
transaction = client.transactions.create_transaction({
|
|
43
42
|
"amount": 1500, # 15.00 MVR
|
|
44
43
|
"currency": "MVR",
|
|
@@ -49,10 +48,6 @@ try:
|
|
|
49
48
|
})
|
|
50
49
|
print(f"Transaction ID: {transaction.transaction_id}")
|
|
51
50
|
print(f"Payment URL: {transaction.url}")
|
|
52
|
-
except Exception as e:
|
|
53
|
-
print(f"Error: {e}")
|
|
54
|
-
finally:
|
|
55
|
-
client.close()
|
|
56
51
|
```
|
|
57
52
|
|
|
58
53
|
### Asynchronous Usage
|
|
@@ -62,14 +57,12 @@ import asyncio
|
|
|
62
57
|
from bml_connect import BMLConnect, Environment
|
|
63
58
|
|
|
64
59
|
async def main():
|
|
65
|
-
|
|
60
|
+
async with BMLConnect(
|
|
66
61
|
api_key="your_api_key",
|
|
67
62
|
app_id="your_app_id",
|
|
68
63
|
environment=Environment.SANDBOX,
|
|
69
64
|
async_mode=True
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
try:
|
|
65
|
+
) as client:
|
|
73
66
|
transaction = await client.transactions.create_transaction({
|
|
74
67
|
"amount": 2000,
|
|
75
68
|
"currency": "MVR",
|
|
@@ -77,8 +70,6 @@ async def main():
|
|
|
77
70
|
"redirectUrl": "https://yourstore.com/success"
|
|
78
71
|
})
|
|
79
72
|
print(f"Transaction ID: {transaction.transaction_id}")
|
|
80
|
-
finally:
|
|
81
|
-
await client.aclose()
|
|
82
73
|
|
|
83
74
|
asyncio.run(main())
|
|
84
75
|
```
|
|
@@ -98,7 +89,7 @@ client = BMLConnect(api_key="your_api_key", app_id="your_app_id")
|
|
|
98
89
|
def webhook():
|
|
99
90
|
payload = request.get_json()
|
|
100
91
|
signature = payload.get('signature')
|
|
101
|
-
|
|
92
|
+
|
|
102
93
|
if client.verify_webhook_signature(payload, signature):
|
|
103
94
|
# Process webhook
|
|
104
95
|
return jsonify({"status": "success"}), 200
|
|
@@ -119,7 +110,7 @@ client = BMLConnect(api_key="your_api_key", app_id="your_app_id")
|
|
|
119
110
|
async def handle_webhook(request: Request):
|
|
120
111
|
payload = await request.json()
|
|
121
112
|
signature = payload.get("signature")
|
|
122
|
-
|
|
113
|
+
|
|
123
114
|
if client.verify_webhook_signature(payload, signature):
|
|
124
115
|
return {"status": "success"}
|
|
125
116
|
else:
|
|
@@ -139,7 +130,7 @@ client = BMLConnect(api_key="your_api_key", app_id="your_app_id")
|
|
|
139
130
|
async def webhook(request):
|
|
140
131
|
payload = request.json
|
|
141
132
|
signature = payload.get('signature')
|
|
142
|
-
|
|
133
|
+
|
|
143
134
|
if client.verify_webhook_signature(payload, signature):
|
|
144
135
|
return response.json({"status": "success"})
|
|
145
136
|
else:
|
|
@@ -148,25 +139,59 @@ async def webhook(request):
|
|
|
148
139
|
|
|
149
140
|
## API Reference
|
|
150
141
|
|
|
142
|
+
### `BMLConnect(api_key, app_id, environment, async_mode, timeout)`
|
|
143
|
+
|
|
144
|
+
Main entry point for the SDK.
|
|
145
|
+
|
|
146
|
+
| Parameter | Type | Default | Description |
|
|
147
|
+
| ------------- | ---------------------- | ------------ | ------------------------------------------------- |
|
|
148
|
+
| `api_key` | `str` | required | Your API key from the BML merchant portal |
|
|
149
|
+
| `app_id` | `str` | required | Your application ID from the BML merchant portal |
|
|
150
|
+
| `environment` | `Environment` or `str` | `PRODUCTION` | `Environment.SANDBOX` or `Environment.PRODUCTION` |
|
|
151
|
+
| `async_mode` | `bool` | `False` | Set `True` to use async methods |
|
|
152
|
+
| `timeout` | `int` | `30` | Request timeout in seconds |
|
|
153
|
+
|
|
154
|
+
### Transaction Methods
|
|
155
|
+
|
|
156
|
+
| Method | Sync | Async | Description |
|
|
157
|
+
| -------------------------- | ---- | ----- | --------------------------------------- |
|
|
158
|
+
| `create_transaction(data)` | ✅ | ✅ | Create a new payment transaction |
|
|
159
|
+
| `get_transaction(id)` | ✅ | ✅ | Retrieve a transaction by ID |
|
|
160
|
+
| `cancel_transaction(id)` | ✅ | ✅ | Cancel a transaction by ID |
|
|
161
|
+
| `list_transactions(...)` | ✅ | ✅ | List transactions with optional filters |
|
|
162
|
+
|
|
163
|
+
### `list_transactions` Parameters
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
client.transactions.list_transactions(
|
|
167
|
+
page=1,
|
|
168
|
+
per_page=20,
|
|
169
|
+
state="CONFIRMED", # Filter by TransactionState value
|
|
170
|
+
provider="alipay", # Filter by provider
|
|
171
|
+
start_date="2026-01-01", # Filter from date
|
|
172
|
+
end_date="2026-02-01", # Filter to date
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
151
176
|
### Core Classes
|
|
152
177
|
|
|
153
178
|
- **`BMLConnect`**: Main entry point for the SDK
|
|
154
|
-
- **`Transaction`**:
|
|
155
|
-
- **`QRCode`**: QR code details
|
|
156
|
-
- **`PaginatedResponse`**:
|
|
157
|
-
- **`Environment`**:
|
|
158
|
-
- **`SignMethod`**:
|
|
159
|
-
- **`TransactionState`**:
|
|
179
|
+
- **`Transaction`**: Typed transaction object returned by all transaction methods
|
|
180
|
+
- **`QRCode`**: QR code details attached to a transaction
|
|
181
|
+
- **`PaginatedResponse`**: Wraps paginated transaction list results
|
|
182
|
+
- **`Environment`**: `SANDBOX` or `PRODUCTION`
|
|
183
|
+
- **`SignMethod`**: `SHA1` (default) or `MD5`
|
|
184
|
+
- **`TransactionState`**: `CREATED`, `QR_CODE_GENERATED`, `CONFIRMED`, `CANCELLED`, `FAILED`, `EXPIRED`, `REFUND_REQUESTED`, `REFUNDED`
|
|
160
185
|
|
|
161
186
|
### Exception Hierarchy
|
|
162
187
|
|
|
163
188
|
```
|
|
164
189
|
BMLConnectError
|
|
165
|
-
├── AuthenticationError
|
|
166
|
-
├── ValidationError
|
|
167
|
-
├── NotFoundError
|
|
168
|
-
├── ServerError
|
|
169
|
-
└── RateLimitError
|
|
190
|
+
├── AuthenticationError (401)
|
|
191
|
+
├── ValidationError (400)
|
|
192
|
+
├── NotFoundError (404)
|
|
193
|
+
├── ServerError (5xx)
|
|
194
|
+
└── RateLimitError (429)
|
|
170
195
|
```
|
|
171
196
|
|
|
172
197
|
### Signature Utilities
|
|
@@ -174,10 +199,10 @@ BMLConnectError
|
|
|
174
199
|
```python
|
|
175
200
|
from bml_connect import SignatureUtils
|
|
176
201
|
|
|
177
|
-
# Generate signature
|
|
202
|
+
# Generate a signature manually
|
|
178
203
|
signature = SignatureUtils.generate_signature(data, api_key, method)
|
|
179
204
|
|
|
180
|
-
# Verify signature
|
|
205
|
+
# Verify a signature with constant-time comparison
|
|
181
206
|
is_valid = SignatureUtils.verify_signature(data, signature, api_key, method)
|
|
182
207
|
```
|
|
183
208
|
|
|
@@ -186,24 +211,31 @@ is_valid = SignatureUtils.verify_signature(data, signature, api_key, method)
|
|
|
186
211
|
### Transaction Management
|
|
187
212
|
|
|
188
213
|
```python
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
with BMLConnect(api_key="your_api_key", app_id="your_app_id") as client:
|
|
215
|
+
# Create
|
|
216
|
+
transaction = client.transactions.create_transaction({
|
|
217
|
+
"amount": 5000,
|
|
218
|
+
"currency": "MVR",
|
|
219
|
+
"provider": "alipay",
|
|
220
|
+
"redirectUrl": "https://yourstore.com/success",
|
|
221
|
+
"localId": "order_456"
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
# Retrieve
|
|
225
|
+
details = client.transactions.get_transaction(transaction.transaction_id)
|
|
226
|
+
print(f"State: {details.state}")
|
|
227
|
+
|
|
228
|
+
# Cancel
|
|
229
|
+
cancelled = client.transactions.cancel_transaction(transaction.transaction_id)
|
|
230
|
+
|
|
231
|
+
# List with filters
|
|
232
|
+
results = client.transactions.list_transactions(
|
|
233
|
+
page=1,
|
|
234
|
+
per_page=10,
|
|
235
|
+
state="CONFIRMED"
|
|
236
|
+
)
|
|
237
|
+
for t in results.items:
|
|
238
|
+
print(t.transaction_id, t.amount, t.state)
|
|
207
239
|
```
|
|
208
240
|
|
|
209
241
|
### Webhook Handling
|
|
@@ -212,39 +244,45 @@ transactions = client.transactions.list_transactions(
|
|
|
212
244
|
@app.route('/webhook', methods=['POST'])
|
|
213
245
|
def handle_webhook():
|
|
214
246
|
payload = request.get_json()
|
|
215
|
-
|
|
216
|
-
# Verify webhook signature
|
|
247
|
+
|
|
217
248
|
if not client.verify_webhook_signature(payload, payload.get('signature')):
|
|
218
249
|
return {"error": "Invalid signature"}, 403
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
pass
|
|
229
|
-
|
|
250
|
+
|
|
251
|
+
state = payload.get('state')
|
|
252
|
+
if state == 'CONFIRMED':
|
|
253
|
+
pass # fulfil the order
|
|
254
|
+
elif state == 'REFUND_REQUESTED':
|
|
255
|
+
pass # initiate refund flow
|
|
256
|
+
elif state == 'REFUNDED':
|
|
257
|
+
pass # mark order as refunded
|
|
258
|
+
|
|
230
259
|
return {"status": "success"}
|
|
231
260
|
```
|
|
232
261
|
|
|
262
|
+
### Custom Timeout
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# For slow network environments or large payloads
|
|
266
|
+
client = BMLConnect(
|
|
267
|
+
api_key="your_api_key",
|
|
268
|
+
app_id="your_app_id",
|
|
269
|
+
timeout=60
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
233
273
|
## Requirements
|
|
234
274
|
|
|
235
|
-
- Python 3.
|
|
236
|
-
-
|
|
275
|
+
- Python 3.9+
|
|
276
|
+
- `requests`
|
|
277
|
+
- `aiohttp`
|
|
237
278
|
|
|
238
279
|
## Development
|
|
239
280
|
|
|
240
|
-
### Setup
|
|
281
|
+
### Setup
|
|
241
282
|
|
|
242
283
|
```bash
|
|
243
|
-
# Clone the repository
|
|
244
284
|
git clone https://github.com/quillfires/bml-connect-python.git
|
|
245
285
|
cd bml-connect-python
|
|
246
|
-
|
|
247
|
-
# Install in development mode
|
|
248
286
|
pip install -e .[dev]
|
|
249
287
|
```
|
|
250
288
|
|
|
@@ -257,13 +295,8 @@ pytest
|
|
|
257
295
|
### Code Quality
|
|
258
296
|
|
|
259
297
|
```bash
|
|
260
|
-
# Format code
|
|
261
298
|
black .
|
|
262
|
-
|
|
263
|
-
# Lint code
|
|
264
299
|
flake8 .
|
|
265
|
-
|
|
266
|
-
# Type checking
|
|
267
300
|
mypy .
|
|
268
301
|
```
|
|
269
302
|
|
|
@@ -282,7 +315,7 @@ bml-connect-python/
|
|
|
282
315
|
|
|
283
316
|
## Contributing
|
|
284
317
|
|
|
285
|
-
|
|
318
|
+
Contributions are welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
286
319
|
|
|
287
320
|
1. Fork the repository
|
|
288
321
|
2. Create a feature branch
|
|
@@ -293,7 +326,7 @@ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.
|
|
|
293
326
|
|
|
294
327
|
## License
|
|
295
328
|
|
|
296
|
-
|
|
329
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
297
330
|
|
|
298
331
|
## Support
|
|
299
332
|
|
|
@@ -303,13 +336,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
303
336
|
|
|
304
337
|
## Changelog
|
|
305
338
|
|
|
306
|
-
See [CHANGELOG.md](CHANGELOG.md) for a
|
|
339
|
+
See [CHANGELOG.md](https://github.com/quillfires/bml-connect-python/blob/main/CHANGELOG.md) for a full history of changes.
|
|
307
340
|
|
|
308
341
|
## Security
|
|
309
342
|
|
|
310
|
-
If you discover
|
|
343
|
+
If you discover a security issue, please email fayaz.quill@gmail.com instead of opening a public issue.
|
|
311
344
|
|
|
312
345
|
---
|
|
313
346
|
|
|
314
|
-
|
|
315
347
|
Made with ❤️ for the Maldivian developer community
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bml-connect-python"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Python SDK for Bank of Maldives Connect API with synchronous and asynchronous support"
|
|
5
5
|
authors = ["Fayaz (Quill) <fayaz.quill@gmail.com>"]
|
|
6
6
|
maintainers = ["Fayaz (Quill) <fayaz.quill@gmail.com>"]
|
|
@@ -49,9 +49,9 @@ class SignMethod(Enum):
|
|
|
49
49
|
@classmethod
|
|
50
50
|
def _missing_(cls, value: object) -> "SignMethod":
|
|
51
51
|
if isinstance(value, str):
|
|
52
|
-
|
|
52
|
+
normalized = value.lower()
|
|
53
53
|
for member in cls:
|
|
54
|
-
if member.value ==
|
|
54
|
+
if member.value == normalized:
|
|
55
55
|
return member
|
|
56
56
|
return cls.SHA1 # Default to SHA1
|
|
57
57
|
|
|
@@ -63,6 +63,8 @@ class TransactionState(Enum):
|
|
|
63
63
|
CANCELLED = "CANCELLED"
|
|
64
64
|
FAILED = "FAILED"
|
|
65
65
|
EXPIRED = "EXPIRED"
|
|
66
|
+
REFUND_REQUESTED = "REFUND_REQUESTED"
|
|
67
|
+
REFUNDED = "REFUNDED"
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
@dataclass
|
|
@@ -103,7 +105,7 @@ class Transaction:
|
|
|
103
105
|
try:
|
|
104
106
|
state = TransactionState(data["state"])
|
|
105
107
|
except ValueError:
|
|
106
|
-
logger.warning(
|
|
108
|
+
logger.warning("Unknown transaction state: %s", data["state"])
|
|
107
109
|
state = None
|
|
108
110
|
|
|
109
111
|
sign_method = None
|
|
@@ -111,7 +113,7 @@ class Transaction:
|
|
|
111
113
|
try:
|
|
112
114
|
sign_method = SignMethod(data["signMethod"])
|
|
113
115
|
except ValueError:
|
|
114
|
-
logger.warning(
|
|
116
|
+
logger.warning("Unknown sign method: %s", data["signMethod"])
|
|
115
117
|
sign_method = None
|
|
116
118
|
|
|
117
119
|
return cls(
|
|
@@ -202,16 +204,17 @@ class SignatureUtils:
|
|
|
202
204
|
) -> str:
|
|
203
205
|
"""Generate signature with proper key sorting and encoding"""
|
|
204
206
|
if isinstance(method, str):
|
|
207
|
+
original_value = method
|
|
205
208
|
try:
|
|
206
209
|
method = SignMethod(method)
|
|
207
210
|
except ValueError:
|
|
211
|
+
logger.warning("Invalid sign method '%s', defaulting to SHA1", original_value)
|
|
208
212
|
method = SignMethod.SHA1
|
|
209
|
-
logger.warning(f"Invalid sign method '{method}', defaulting to SHA1")
|
|
210
213
|
|
|
211
214
|
amount = data.get("amount")
|
|
212
215
|
currency = data.get("currency")
|
|
213
216
|
|
|
214
|
-
if
|
|
217
|
+
if amount is None or not currency:
|
|
215
218
|
raise ValueError(
|
|
216
219
|
"Amount and currency are required for signature generation"
|
|
217
220
|
)
|
|
@@ -245,19 +248,19 @@ class BaseClient:
|
|
|
245
248
|
api_key: str,
|
|
246
249
|
app_id: str,
|
|
247
250
|
environment: Environment = Environment.PRODUCTION,
|
|
251
|
+
timeout: int = 30,
|
|
248
252
|
):
|
|
249
253
|
self.api_key = api_key
|
|
250
254
|
self.app_id = app_id
|
|
251
255
|
self.environment = environment
|
|
252
256
|
self.base_url = environment.base_url
|
|
253
|
-
self.
|
|
254
|
-
|
|
255
|
-
)
|
|
257
|
+
self.timeout = timeout
|
|
258
|
+
self.session: Optional[Union[requests.Session, aiohttp.ClientSession]] = None
|
|
256
259
|
logger.info("Initialized BML Client for %s environment", environment.name)
|
|
257
260
|
|
|
258
261
|
def _get_headers(self) -> Dict[str, str]:
|
|
259
262
|
return {
|
|
260
|
-
"Authorization":
|
|
263
|
+
"Authorization": self.api_key,
|
|
261
264
|
"Accept": "application/json",
|
|
262
265
|
"Content-Type": "application/json",
|
|
263
266
|
"User-Agent": USER_AGENT,
|
|
@@ -310,12 +313,18 @@ class SyncClient(BaseClient):
|
|
|
310
313
|
self.session.headers.update(self._get_headers())
|
|
311
314
|
logger.debug("Initialized synchronous HTTP session")
|
|
312
315
|
|
|
316
|
+
def __enter__(self) -> "SyncClient":
|
|
317
|
+
return self
|
|
318
|
+
|
|
319
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
320
|
+
self.close()
|
|
321
|
+
|
|
313
322
|
def _request(self, method: str, endpoint: str, **kwargs: Any) -> Dict[str, Any]:
|
|
314
323
|
url = f"{self.base_url}{endpoint}"
|
|
315
324
|
logger.debug("Request: %s %s %s", method, url, kwargs.get("params"))
|
|
316
325
|
|
|
317
326
|
try:
|
|
318
|
-
response = self.session.request(method, url, timeout=
|
|
327
|
+
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
|
319
328
|
|
|
320
329
|
try:
|
|
321
330
|
response_data: Dict[str, Any] = response.json()
|
|
@@ -357,6 +366,12 @@ class SyncClient(BaseClient):
|
|
|
357
366
|
response = self._request("GET", f"/transactions/{transaction_id}")
|
|
358
367
|
return Transaction.from_dict(response)
|
|
359
368
|
|
|
369
|
+
def cancel_transaction(self, transaction_id: str) -> Transaction:
|
|
370
|
+
"""Cancel a transaction by ID"""
|
|
371
|
+
logger.info("Cancelling transaction: %s", transaction_id)
|
|
372
|
+
response = self._request("DELETE", f"/transactions/{transaction_id}")
|
|
373
|
+
return Transaction.from_dict(response)
|
|
374
|
+
|
|
360
375
|
def list_transactions(
|
|
361
376
|
self,
|
|
362
377
|
page: int = 1,
|
|
@@ -369,14 +384,14 @@ class SyncClient(BaseClient):
|
|
|
369
384
|
logger.info("Listing transactions: page=%s, per_page=%s", page, per_page)
|
|
370
385
|
params: Dict[str, Any] = {"page": page, "perPage": per_page}
|
|
371
386
|
|
|
372
|
-
#
|
|
373
|
-
if state:
|
|
387
|
+
# FIX: Use `is not None` so that an explicit empty string isn't silently dropped
|
|
388
|
+
if state is not None:
|
|
374
389
|
params["state"] = state
|
|
375
|
-
if provider:
|
|
390
|
+
if provider is not None:
|
|
376
391
|
params["provider"] = provider
|
|
377
|
-
if start_date:
|
|
392
|
+
if start_date is not None:
|
|
378
393
|
params["startDate"] = start_date
|
|
379
|
-
if end_date:
|
|
394
|
+
if end_date is not None:
|
|
380
395
|
params["endDate"] = end_date
|
|
381
396
|
|
|
382
397
|
response = self._request("GET", "/transactions", params=params)
|
|
@@ -392,10 +407,24 @@ class SyncClient(BaseClient):
|
|
|
392
407
|
class AsyncClient(BaseClient):
|
|
393
408
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
394
409
|
super().__init__(*args, **kwargs)
|
|
395
|
-
self.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
410
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
411
|
+
logger.debug("Initialized asynchronous client (session deferred)")
|
|
412
|
+
|
|
413
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
417
|
+
await self.close()
|
|
418
|
+
|
|
419
|
+
def _get_session(self) -> aiohttp.ClientSession:
|
|
420
|
+
"""Lazily create the aiohttp session within an async context"""
|
|
421
|
+
if self._session is None or self._session.closed:
|
|
422
|
+
self._session = aiohttp.ClientSession(
|
|
423
|
+
headers=self._get_headers(),
|
|
424
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
425
|
+
)
|
|
426
|
+
logger.debug("Created asynchronous HTTP session")
|
|
427
|
+
return self._session
|
|
399
428
|
|
|
400
429
|
async def _request(
|
|
401
430
|
self, method: str, endpoint: str, **kwargs: Any
|
|
@@ -403,8 +432,9 @@ class AsyncClient(BaseClient):
|
|
|
403
432
|
url = f"{self.base_url}{endpoint}"
|
|
404
433
|
logger.debug("Async Request: %s %s %s", method, url, kwargs.get("params"))
|
|
405
434
|
|
|
435
|
+
session = self._get_session()
|
|
406
436
|
try:
|
|
407
|
-
async with
|
|
437
|
+
async with session.request(method, url, **kwargs) as response:
|
|
408
438
|
try:
|
|
409
439
|
response_data: Dict[str, Any] = await response.json()
|
|
410
440
|
except aiohttp.ContentTypeError:
|
|
@@ -446,6 +476,12 @@ class AsyncClient(BaseClient):
|
|
|
446
476
|
response = await self._request("GET", f"/transactions/{transaction_id}")
|
|
447
477
|
return Transaction.from_dict(response)
|
|
448
478
|
|
|
479
|
+
async def cancel_transaction(self, transaction_id: str) -> Transaction:
|
|
480
|
+
"""Cancel a transaction by ID"""
|
|
481
|
+
logger.info("Cancelling transaction (async): %s", transaction_id)
|
|
482
|
+
response = await self._request("DELETE", f"/transactions/{transaction_id}")
|
|
483
|
+
return Transaction.from_dict(response)
|
|
484
|
+
|
|
449
485
|
async def list_transactions(
|
|
450
486
|
self,
|
|
451
487
|
page: int = 1,
|
|
@@ -460,14 +496,14 @@ class AsyncClient(BaseClient):
|
|
|
460
496
|
)
|
|
461
497
|
params: Dict[str, Any] = {"page": page, "perPage": per_page}
|
|
462
498
|
|
|
463
|
-
#
|
|
464
|
-
if state:
|
|
499
|
+
# FIX: Use `is not None` so that an explicit empty string isn't silently dropped
|
|
500
|
+
if state is not None:
|
|
465
501
|
params["state"] = state
|
|
466
|
-
if provider:
|
|
502
|
+
if provider is not None:
|
|
467
503
|
params["provider"] = provider
|
|
468
|
-
if start_date:
|
|
504
|
+
if start_date is not None:
|
|
469
505
|
params["startDate"] = start_date
|
|
470
|
-
if end_date:
|
|
506
|
+
if end_date is not None:
|
|
471
507
|
params["endDate"] = end_date
|
|
472
508
|
|
|
473
509
|
response = await self._request("GET", "/transactions", params=params)
|
|
@@ -475,8 +511,8 @@ class AsyncClient(BaseClient):
|
|
|
475
511
|
|
|
476
512
|
async def close(self) -> None:
|
|
477
513
|
"""Close the HTTP session"""
|
|
478
|
-
if self.
|
|
479
|
-
await self.
|
|
514
|
+
if self._session and not self._session.closed:
|
|
515
|
+
await self._session.close()
|
|
480
516
|
logger.debug("Closed asynchronous HTTP session")
|
|
481
517
|
|
|
482
518
|
|
|
@@ -487,6 +523,7 @@ class BMLConnect:
|
|
|
487
523
|
app_id: str,
|
|
488
524
|
environment: Union[Environment, str] = Environment.PRODUCTION,
|
|
489
525
|
async_mode: bool = False,
|
|
526
|
+
timeout: int = 30,
|
|
490
527
|
):
|
|
491
528
|
"""
|
|
492
529
|
Initialize BML Connect client
|
|
@@ -496,6 +533,7 @@ class BMLConnect:
|
|
|
496
533
|
app_id: Your application ID from BML merchant portal
|
|
497
534
|
environment: 'production' or 'sandbox' (default: production)
|
|
498
535
|
async_mode: Whether to use async operations (default: False)
|
|
536
|
+
timeout: Request timeout in seconds (default: 30)
|
|
499
537
|
"""
|
|
500
538
|
self.api_key = api_key
|
|
501
539
|
self.app_id = app_id
|
|
@@ -514,9 +552,29 @@ class BMLConnect:
|
|
|
514
552
|
self.client: Union[SyncClient, AsyncClient]
|
|
515
553
|
|
|
516
554
|
if async_mode:
|
|
517
|
-
self.client = AsyncClient(api_key, app_id, self.environment)
|
|
555
|
+
self.client = AsyncClient(api_key, app_id, self.environment, timeout)
|
|
518
556
|
else:
|
|
519
|
-
self.client = SyncClient(api_key, app_id, self.environment)
|
|
557
|
+
self.client = SyncClient(api_key, app_id, self.environment, timeout)
|
|
558
|
+
|
|
559
|
+
def __enter__(self) -> "BMLConnect":
|
|
560
|
+
if isinstance(self.client, AsyncClient):
|
|
561
|
+
raise TypeError(
|
|
562
|
+
"Use 'async with' for async_mode=True clients"
|
|
563
|
+
)
|
|
564
|
+
return self
|
|
565
|
+
|
|
566
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
567
|
+
self.close()
|
|
568
|
+
|
|
569
|
+
async def __aenter__(self) -> "BMLConnect":
|
|
570
|
+
if isinstance(self.client, SyncClient):
|
|
571
|
+
raise TypeError(
|
|
572
|
+
"Use 'with' (not 'async with') for async_mode=False clients"
|
|
573
|
+
)
|
|
574
|
+
return self
|
|
575
|
+
|
|
576
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
577
|
+
await self.aclose()
|
|
520
578
|
|
|
521
579
|
@property
|
|
522
580
|
def transactions(self) -> Union[SyncClient, AsyncClient]:
|
|
@@ -545,10 +603,10 @@ class BMLConnect:
|
|
|
545
603
|
except json.JSONDecodeError:
|
|
546
604
|
raise ValidationError("Invalid JSON payload")
|
|
547
605
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
verification_payload =
|
|
606
|
+
if not isinstance(payload, dict):
|
|
607
|
+
raise ValidationError("Payload must be a JSON object")
|
|
608
|
+
|
|
609
|
+
verification_payload = payload.copy()
|
|
552
610
|
if "signature" in verification_payload:
|
|
553
611
|
del verification_payload["signature"]
|
|
554
612
|
|
|
@@ -582,4 +640,4 @@ __all__ = [
|
|
|
582
640
|
"ServerError",
|
|
583
641
|
"RateLimitError",
|
|
584
642
|
"SignatureUtils",
|
|
585
|
-
]
|
|
643
|
+
]
|
|
File without changes
|
|
File without changes
|