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.
Files changed (55) hide show
  1. cinetpay_python-0.1.0/.gitignore +16 -0
  2. cinetpay_python-0.1.0/PKG-INFO +376 -0
  3. cinetpay_python-0.1.0/README.md +347 -0
  4. cinetpay_python-0.1.0/pyproject.toml +55 -0
  5. cinetpay_python-0.1.0/src/cinetpay/__init__.py +175 -0
  6. cinetpay_python-0.1.0/src/cinetpay/api/__init__.py +17 -0
  7. cinetpay_python-0.1.0/src/cinetpay/api/async_balance.py +70 -0
  8. cinetpay_python-0.1.0/src/cinetpay/api/async_payment.py +118 -0
  9. cinetpay_python-0.1.0/src/cinetpay/api/async_transfer.py +118 -0
  10. cinetpay_python-0.1.0/src/cinetpay/api/balance.py +70 -0
  11. cinetpay_python-0.1.0/src/cinetpay/api/payment.py +118 -0
  12. cinetpay_python-0.1.0/src/cinetpay/api/transfer.py +118 -0
  13. cinetpay_python-0.1.0/src/cinetpay/async_client.py +290 -0
  14. cinetpay_python-0.1.0/src/cinetpay/auth/__init__.py +11 -0
  15. cinetpay_python-0.1.0/src/cinetpay/auth/async_authenticator.py +119 -0
  16. cinetpay_python-0.1.0/src/cinetpay/auth/authenticator.py +140 -0
  17. cinetpay_python-0.1.0/src/cinetpay/auth/token_store.py +72 -0
  18. cinetpay_python-0.1.0/src/cinetpay/client.py +256 -0
  19. cinetpay_python-0.1.0/src/cinetpay/constants/__init__.py +15 -0
  20. cinetpay_python-0.1.0/src/cinetpay/constants/channels.py +12 -0
  21. cinetpay_python-0.1.0/src/cinetpay/constants/currencies.py +14 -0
  22. cinetpay_python-0.1.0/src/cinetpay/constants/payment_methods.py +43 -0
  23. cinetpay_python-0.1.0/src/cinetpay/constants/statuses.py +44 -0
  24. cinetpay_python-0.1.0/src/cinetpay/errors/__init__.py +14 -0
  25. cinetpay_python-0.1.0/src/cinetpay/errors/api_error.py +45 -0
  26. cinetpay_python-0.1.0/src/cinetpay/errors/auth_error.py +12 -0
  27. cinetpay_python-0.1.0/src/cinetpay/errors/base.py +18 -0
  28. cinetpay_python-0.1.0/src/cinetpay/errors/network_errors.py +29 -0
  29. cinetpay_python-0.1.0/src/cinetpay/http/__init__.py +9 -0
  30. cinetpay_python-0.1.0/src/cinetpay/http/async_http_client.py +173 -0
  31. cinetpay_python-0.1.0/src/cinetpay/http/http_client.py +173 -0
  32. cinetpay_python-0.1.0/src/cinetpay/logger.py +70 -0
  33. cinetpay_python-0.1.0/src/cinetpay/py.typed +0 -0
  34. cinetpay_python-0.1.0/src/cinetpay/types/__init__.py +59 -0
  35. cinetpay_python-0.1.0/src/cinetpay/types/balance.py +35 -0
  36. cinetpay_python-0.1.0/src/cinetpay/types/config.py +139 -0
  37. cinetpay_python-0.1.0/src/cinetpay/types/payment.py +210 -0
  38. cinetpay_python-0.1.0/src/cinetpay/types/transfer.py +163 -0
  39. cinetpay_python-0.1.0/src/cinetpay/types/webhook.py +59 -0
  40. cinetpay_python-0.1.0/src/cinetpay/validation.py +163 -0
  41. cinetpay_python-0.1.0/src/cinetpay/webhooks/__init__.py +8 -0
  42. cinetpay_python-0.1.0/src/cinetpay/webhooks/notification.py +79 -0
  43. cinetpay_python-0.1.0/tests/__init__.py +0 -0
  44. cinetpay_python-0.1.0/tests/conftest.py +152 -0
  45. cinetpay_python-0.1.0/tests/test_authenticator.py +206 -0
  46. cinetpay_python-0.1.0/tests/test_balance_api.py +135 -0
  47. cinetpay_python-0.1.0/tests/test_client.py +318 -0
  48. cinetpay_python-0.1.0/tests/test_constants.py +227 -0
  49. cinetpay_python-0.1.0/tests/test_errors.py +171 -0
  50. cinetpay_python-0.1.0/tests/test_logger.py +91 -0
  51. cinetpay_python-0.1.0/tests/test_payment_api.py +213 -0
  52. cinetpay_python-0.1.0/tests/test_token_store.py +111 -0
  53. cinetpay_python-0.1.0/tests/test_transfer_api.py +207 -0
  54. cinetpay_python-0.1.0/tests/test_validation.py +361 -0
  55. cinetpay_python-0.1.0/tests/test_webhook.py +141 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .mypy_cache/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ htmlcov/
12
+ .coverage
13
+ .env
14
+ .venv/
15
+ venv/
16
+ .DS_Store
@@ -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