cinetpay-python 0.1.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.
- cinetpay_python-0.1.0/.gitignore +16 -0
- cinetpay_python-0.1.0/PKG-INFO +376 -0
- cinetpay_python-0.1.0/README.md +347 -0
- cinetpay_python-0.1.0/pyproject.toml +55 -0
- cinetpay_python-0.1.0/src/cinetpay/__init__.py +175 -0
- cinetpay_python-0.1.0/src/cinetpay/api/__init__.py +17 -0
- cinetpay_python-0.1.0/src/cinetpay/api/async_balance.py +70 -0
- cinetpay_python-0.1.0/src/cinetpay/api/async_payment.py +118 -0
- cinetpay_python-0.1.0/src/cinetpay/api/async_transfer.py +118 -0
- cinetpay_python-0.1.0/src/cinetpay/api/balance.py +70 -0
- cinetpay_python-0.1.0/src/cinetpay/api/payment.py +118 -0
- cinetpay_python-0.1.0/src/cinetpay/api/transfer.py +118 -0
- cinetpay_python-0.1.0/src/cinetpay/async_client.py +290 -0
- cinetpay_python-0.1.0/src/cinetpay/auth/__init__.py +11 -0
- cinetpay_python-0.1.0/src/cinetpay/auth/async_authenticator.py +119 -0
- cinetpay_python-0.1.0/src/cinetpay/auth/authenticator.py +140 -0
- cinetpay_python-0.1.0/src/cinetpay/auth/token_store.py +72 -0
- cinetpay_python-0.1.0/src/cinetpay/client.py +256 -0
- cinetpay_python-0.1.0/src/cinetpay/constants/__init__.py +15 -0
- cinetpay_python-0.1.0/src/cinetpay/constants/channels.py +12 -0
- cinetpay_python-0.1.0/src/cinetpay/constants/currencies.py +14 -0
- cinetpay_python-0.1.0/src/cinetpay/constants/payment_methods.py +43 -0
- cinetpay_python-0.1.0/src/cinetpay/constants/statuses.py +44 -0
- cinetpay_python-0.1.0/src/cinetpay/errors/__init__.py +14 -0
- cinetpay_python-0.1.0/src/cinetpay/errors/api_error.py +45 -0
- cinetpay_python-0.1.0/src/cinetpay/errors/auth_error.py +12 -0
- cinetpay_python-0.1.0/src/cinetpay/errors/base.py +18 -0
- cinetpay_python-0.1.0/src/cinetpay/errors/network_errors.py +29 -0
- cinetpay_python-0.1.0/src/cinetpay/http/__init__.py +9 -0
- cinetpay_python-0.1.0/src/cinetpay/http/async_http_client.py +173 -0
- cinetpay_python-0.1.0/src/cinetpay/http/http_client.py +173 -0
- cinetpay_python-0.1.0/src/cinetpay/logger.py +70 -0
- cinetpay_python-0.1.0/src/cinetpay/py.typed +0 -0
- cinetpay_python-0.1.0/src/cinetpay/types/__init__.py +59 -0
- cinetpay_python-0.1.0/src/cinetpay/types/balance.py +35 -0
- cinetpay_python-0.1.0/src/cinetpay/types/config.py +139 -0
- cinetpay_python-0.1.0/src/cinetpay/types/payment.py +210 -0
- cinetpay_python-0.1.0/src/cinetpay/types/transfer.py +163 -0
- cinetpay_python-0.1.0/src/cinetpay/types/webhook.py +59 -0
- cinetpay_python-0.1.0/src/cinetpay/validation.py +163 -0
- cinetpay_python-0.1.0/src/cinetpay/webhooks/__init__.py +8 -0
- cinetpay_python-0.1.0/src/cinetpay/webhooks/notification.py +79 -0
- cinetpay_python-0.1.0/tests/__init__.py +0 -0
- cinetpay_python-0.1.0/tests/conftest.py +152 -0
- cinetpay_python-0.1.0/tests/test_authenticator.py +206 -0
- cinetpay_python-0.1.0/tests/test_balance_api.py +135 -0
- cinetpay_python-0.1.0/tests/test_client.py +318 -0
- cinetpay_python-0.1.0/tests/test_constants.py +227 -0
- cinetpay_python-0.1.0/tests/test_errors.py +171 -0
- cinetpay_python-0.1.0/tests/test_logger.py +91 -0
- cinetpay_python-0.1.0/tests/test_payment_api.py +213 -0
- cinetpay_python-0.1.0/tests/test_token_store.py +111 -0
- cinetpay_python-0.1.0/tests/test_transfer_api.py +207 -0
- cinetpay_python-0.1.0/tests/test_validation.py +361 -0
- cinetpay_python-0.1.0/tests/test_webhook.py +141 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cinetpay-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK Python pour l'API CinetPay v1 — paiements et transferts mobile money en Afrique. Compatible Django, FastAPI, Flask et tout projet Python.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cinetpay/cinetpay-python
|
|
6
|
+
Project-URL: Documentation, https://docs.cinetpay.com
|
|
7
|
+
Author: CinetPay
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: africa,cinetpay,mobile-money,mtn,orange-money,payment,sdk,wave
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: coverage[toml]>=7.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# cinetpay-python
|
|
31
|
+
|
|
32
|
+
SDK Python pour l'API CinetPay v1 — paiements et transferts mobile money en Afrique.
|
|
33
|
+
|
|
34
|
+
Compatible Django, FastAPI, Flask et tout projet Python 3.10+.
|
|
35
|
+
|
|
36
|
+
## Caractéristiques
|
|
37
|
+
|
|
38
|
+
- **Sync + Async** : `CinetPayClient` et `AsyncCinetPayClient`
|
|
39
|
+
- **Multi-pays** : credentials `api_key` / `api_password` par pays
|
|
40
|
+
- **Auto-détection** : sandbox (`sk_test_`) vs production (`sk_live_`)
|
|
41
|
+
- **Token cache** : JWT mis en cache 23h, thread-safe (stampede guard)
|
|
42
|
+
- **Validation** : données validées avant envoi (montants, emails, URLs)
|
|
43
|
+
- **Webhook** : vérification timing-safe (`hmac.compare_digest`)
|
|
44
|
+
- **Typé** : type hints complets, `py.typed` (PEP 561), compatible mypy
|
|
45
|
+
- **Sécurisé** : HTTPS obligatoire, credentials masqués dans `repr()`, SSRF protection
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install cinetpay-python
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Environnements
|
|
54
|
+
|
|
55
|
+
| Préfixe clé API | URL API | Environnement |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `sk_test_...` | `https://api.cinetpay.net` | Sandbox |
|
|
58
|
+
| `sk_live_...` | `https://api.cinetpay.co` | Production |
|
|
59
|
+
|
|
60
|
+
Le SDK détecte automatiquement l'environnement à partir du préfixe de la clé.
|
|
61
|
+
|
|
62
|
+
## Démarrage rapide
|
|
63
|
+
|
|
64
|
+
### Synchrone
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from cinetpay import CinetPayClient, ClientConfig, CountryCredentials, PaymentRequest
|
|
68
|
+
import os
|
|
69
|
+
|
|
70
|
+
client = CinetPayClient(ClientConfig(
|
|
71
|
+
credentials={
|
|
72
|
+
"CI": CountryCredentials(
|
|
73
|
+
api_key=os.environ["CINETPAY_API_KEY_CI"],
|
|
74
|
+
api_password=os.environ["CINETPAY_API_PASSWORD_CI"],
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
debug=True,
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
# Initialiser un paiement
|
|
81
|
+
payment = client.payment.initialize(
|
|
82
|
+
PaymentRequest(
|
|
83
|
+
currency="XOF",
|
|
84
|
+
merchant_transaction_id="ORDER-001",
|
|
85
|
+
amount=5000,
|
|
86
|
+
lang="fr",
|
|
87
|
+
designation="Achat en ligne",
|
|
88
|
+
client_email="client@email.com",
|
|
89
|
+
client_first_name="Jean",
|
|
90
|
+
client_last_name="Dupont",
|
|
91
|
+
success_url="https://monsite.com/success",
|
|
92
|
+
failed_url="https://monsite.com/failed",
|
|
93
|
+
notify_url="https://monsite.com/webhook",
|
|
94
|
+
channel="PUSH",
|
|
95
|
+
),
|
|
96
|
+
"CI",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
print(payment.payment_url) # Rediriger le client
|
|
100
|
+
print(payment.payment_token) # Pour le Seamless frontend
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Asynchrone
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
from cinetpay import AsyncCinetPayClient, ClientConfig, CountryCredentials
|
|
108
|
+
|
|
109
|
+
async def main():
|
|
110
|
+
async with AsyncCinetPayClient(ClientConfig(
|
|
111
|
+
credentials={
|
|
112
|
+
"CI": CountryCredentials(
|
|
113
|
+
api_key="sk_test_...",
|
|
114
|
+
api_password="your_password",
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
)) as client:
|
|
118
|
+
balance = await client.balance.get("CI")
|
|
119
|
+
print(f"Solde: {balance.available_balance} {balance.currency}")
|
|
120
|
+
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## API
|
|
125
|
+
|
|
126
|
+
### Paiement
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Initialiser
|
|
130
|
+
payment = client.payment.initialize(PaymentRequest(...), "CI")
|
|
131
|
+
print(payment.payment_url)
|
|
132
|
+
print(payment.payment_token)
|
|
133
|
+
|
|
134
|
+
# Vérifier le statut
|
|
135
|
+
status = client.payment.get_status("ORDER-001", "CI")
|
|
136
|
+
print(status.status) # SUCCESS, FAILED, PENDING, ...
|
|
137
|
+
print(status.user.name)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Transfert
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from cinetpay import TransferRequest
|
|
144
|
+
|
|
145
|
+
transfer = client.transfer.create(
|
|
146
|
+
TransferRequest(
|
|
147
|
+
currency="XOF",
|
|
148
|
+
merchant_transaction_id="TR-001",
|
|
149
|
+
phone_number="+2250707000001",
|
|
150
|
+
amount=500,
|
|
151
|
+
payment_method="OM_CI",
|
|
152
|
+
reason="Remboursement",
|
|
153
|
+
notify_url="https://monsite.com/webhook",
|
|
154
|
+
),
|
|
155
|
+
"CI",
|
|
156
|
+
)
|
|
157
|
+
print(transfer.status)
|
|
158
|
+
|
|
159
|
+
# Vérifier le statut
|
|
160
|
+
status = client.transfer.get_status(transfer.transaction_id, "CI")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Solde
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
balance = client.balance.get("CI")
|
|
167
|
+
print(f"{balance.available_balance} {balance.currency}")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Webhook
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from cinetpay import verify_notification, parse_notification
|
|
174
|
+
|
|
175
|
+
# Flask
|
|
176
|
+
@app.route("/webhook", methods=["POST"])
|
|
177
|
+
def webhook():
|
|
178
|
+
payload = parse_notification(request.json)
|
|
179
|
+
|
|
180
|
+
# Vérifier le token (timing-safe)
|
|
181
|
+
expected = get_stored_notify_token(payload.merchant_transaction_id)
|
|
182
|
+
if not verify_notification(expected, payload.notify_token):
|
|
183
|
+
return "Invalid token", 401
|
|
184
|
+
|
|
185
|
+
# Confirmer le statut
|
|
186
|
+
status = client.payment.get_status(payload.transaction_id, "CI")
|
|
187
|
+
if status.status == "SUCCESS":
|
|
188
|
+
# Livrer la commande
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
return "OK", 200
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
# FastAPI
|
|
196
|
+
@app.post("/webhook")
|
|
197
|
+
async def webhook(request: Request):
|
|
198
|
+
body = await request.json()
|
|
199
|
+
payload = parse_notification(body)
|
|
200
|
+
|
|
201
|
+
if not verify_notification(stored_token, payload.notify_token):
|
|
202
|
+
raise HTTPException(401, "Invalid token")
|
|
203
|
+
|
|
204
|
+
status = await client.payment.get_status(payload.transaction_id, "CI")
|
|
205
|
+
return {"status": status.status}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Django
|
|
210
|
+
def webhook(request):
|
|
211
|
+
import json
|
|
212
|
+
payload = parse_notification(json.loads(request.body))
|
|
213
|
+
|
|
214
|
+
if not verify_notification(stored_token, payload.notify_token):
|
|
215
|
+
return HttpResponse(status=401)
|
|
216
|
+
|
|
217
|
+
status = client.payment.get_status(payload.transaction_id, "CI")
|
|
218
|
+
return HttpResponse("OK")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Configuration
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from cinetpay import ClientConfig, CountryCredentials
|
|
225
|
+
|
|
226
|
+
config = ClientConfig(
|
|
227
|
+
# Credentials par pays (obligatoire)
|
|
228
|
+
credentials={
|
|
229
|
+
"CI": CountryCredentials(api_key="sk_test_...", api_password="..."),
|
|
230
|
+
"SN": CountryCredentials(api_key="sk_test_...", api_password="..."),
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
# URL de base (auto-détecté depuis le préfixe de la clé)
|
|
234
|
+
# base_url="https://api.cinetpay.co", # forcer la production
|
|
235
|
+
|
|
236
|
+
# TTL du cache token en secondes (défaut: 82800 = 23h)
|
|
237
|
+
token_ttl=82800,
|
|
238
|
+
|
|
239
|
+
# Timeout des requêtes en secondes (défaut: 30.0)
|
|
240
|
+
timeout=30.0,
|
|
241
|
+
|
|
242
|
+
# Active les logs (défaut: False)
|
|
243
|
+
debug=True,
|
|
244
|
+
|
|
245
|
+
# Token store personnalisé (défaut: MemoryTokenStore)
|
|
246
|
+
# token_store=RedisTokenStore(),
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Token store Redis
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import redis
|
|
254
|
+
from cinetpay import ClientConfig, CountryCredentials
|
|
255
|
+
|
|
256
|
+
class RedisTokenStore:
|
|
257
|
+
def __init__(self):
|
|
258
|
+
self.r = redis.Redis()
|
|
259
|
+
|
|
260
|
+
def get(self, key: str) -> str | None:
|
|
261
|
+
val = self.r.get(key)
|
|
262
|
+
return val.decode() if val else None
|
|
263
|
+
|
|
264
|
+
def set(self, key: str, value: str, ttl_seconds: int) -> None:
|
|
265
|
+
self.r.setex(key, ttl_seconds, value)
|
|
266
|
+
|
|
267
|
+
def delete(self, key: str) -> None:
|
|
268
|
+
self.r.delete(key)
|
|
269
|
+
|
|
270
|
+
client = CinetPayClient(ClientConfig(
|
|
271
|
+
credentials={"CI": CountryCredentials(...)},
|
|
272
|
+
token_store=RedisTokenStore(),
|
|
273
|
+
))
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Gestion des erreurs
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from cinetpay import (
|
|
280
|
+
CinetPayError,
|
|
281
|
+
ApiError,
|
|
282
|
+
AuthenticationError,
|
|
283
|
+
NetworkError,
|
|
284
|
+
ValidationError,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
payment = client.payment.initialize(request, "CI")
|
|
289
|
+
except ValidationError as e:
|
|
290
|
+
# Données invalides — avant tout appel réseau
|
|
291
|
+
print(e) # [amount] must be an integer between 100 and 2500000
|
|
292
|
+
|
|
293
|
+
except ApiError as e:
|
|
294
|
+
# Erreur API CinetPay
|
|
295
|
+
print(e.api_code) # 1200
|
|
296
|
+
print(e.api_status) # TRANSACTION_EXIST
|
|
297
|
+
print(e.description) # La transaction existe déjà
|
|
298
|
+
|
|
299
|
+
except AuthenticationError:
|
|
300
|
+
# Credentials invalides
|
|
301
|
+
|
|
302
|
+
except NetworkError as e:
|
|
303
|
+
# Problème réseau
|
|
304
|
+
print(e.cause)
|
|
305
|
+
|
|
306
|
+
except CinetPayError:
|
|
307
|
+
# Catch-all pour toutes les erreurs du SDK
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Utilitaires
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from cinetpay import is_final_status, PAYMENT_METHODS_BY_COUNTRY, COUNTRY_CODES
|
|
314
|
+
|
|
315
|
+
# Vérifier si un statut est final
|
|
316
|
+
is_final_status("SUCCESS") # True
|
|
317
|
+
is_final_status("PENDING") # False
|
|
318
|
+
|
|
319
|
+
# Opérateurs par pays
|
|
320
|
+
PAYMENT_METHODS_BY_COUNTRY["CI"] # ("OM_CI", "MOOV_CI", "MTN_CI", "WAVE_CI")
|
|
321
|
+
|
|
322
|
+
# Pays supportés
|
|
323
|
+
COUNTRY_CODES # ("CI", "BF", "ML", "SN", "TG", "GN", "CM", "BJ", "CD", "NE")
|
|
324
|
+
|
|
325
|
+
# Révoquer un token
|
|
326
|
+
client.revoke_token("CI")
|
|
327
|
+
client.revoke_all_tokens()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Context Manager
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
# Sync
|
|
334
|
+
with CinetPayClient(config) as client:
|
|
335
|
+
balance = client.balance.get("CI")
|
|
336
|
+
|
|
337
|
+
# Async
|
|
338
|
+
async with AsyncCinetPayClient(config) as client:
|
|
339
|
+
balance = await client.balance.get("CI")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Sécurité
|
|
343
|
+
|
|
344
|
+
### Protection des clés API
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
NE FAITES PAS FAITES
|
|
348
|
+
────────────────────────────────────────────────────────────────────────
|
|
349
|
+
api_key="clé-en-dur" api_key=os.environ["CINETPAY_API_KEY_CI"]
|
|
350
|
+
Mélanger sk_test_ et sk_live_ Utiliser le même env pour tous les pays
|
|
351
|
+
Commiter le .env dans git Ajouter .env dans .gitignore
|
|
352
|
+
print(credentials) Le repr() masque automatiquement les clés
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Credentials masqués
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
creds = CountryCredentials(api_key="sk_test_abc", api_password="secret")
|
|
359
|
+
print(creds) # CountryCredentials(api_key='***', api_password='***')
|
|
360
|
+
print(client) # CinetPayClient(countries=['CI', 'SN'])
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Autres protections
|
|
364
|
+
|
|
365
|
+
- **HTTPS obligatoire** (sauf localhost)
|
|
366
|
+
- **SSRF** : warning si le hostname n'est pas un domaine CinetPay connu
|
|
367
|
+
- **Erreurs sanitisées** : les messages d'erreur d'authentification ne contiennent jamais les credentials
|
|
368
|
+
- **Token stampede guard** : `threading.Lock` (sync) / `asyncio.Lock` (async) empêche les appels auth simultanés
|
|
369
|
+
|
|
370
|
+
## Support
|
|
371
|
+
|
|
372
|
+
Pour toute question sur l'API CinetPay : **support@cinetpay.com**
|
|
373
|
+
|
|
374
|
+
## Licence
|
|
375
|
+
|
|
376
|
+
MIT
|