fameen-messaging 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.
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ .eggs/
7
+
8
+ # Environnements virtuels
9
+ .venv/
10
+ venv/
11
+ env/
12
+
13
+ # Build / distribution
14
+ build/
15
+ dist/
16
+ wheels/
17
+
18
+ # Outils
19
+ .pytest_cache/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+ .coverage
23
+ coverage.xml
24
+ htmlcov/
25
+ .tox/
26
+
27
+ # IDE / OS
28
+ .idea/
29
+ .vscode/
30
+ .DS_Store
31
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fameen Groupe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,287 @@
1
+ Metadata-Version: 2.4
2
+ Name: fameen-messaging
3
+ Version: 0.1.0
4
+ Summary: SDK Python officiel de l'API Fameen Messaging — SMS, WhatsApp et Email (envoi, suivi, webhooks).
5
+ Project-URL: Homepage, https://business.fameengroupe.com
6
+ Author: Fameen Groupe
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: email,fameen,messaging,notifications,sms,whatsapp
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Communications
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: httpx>=0.27
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
29
+ Requires-Dist: pytest>=8.0; extra == 'test'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Fameen Messaging — SDK Python officiel
33
+
34
+ SDK Python officiel de l'API **Fameen Messaging** : envoyez des **SMS**, des messages **WhatsApp** et des **emails** depuis vos applications Python, suivez leur statut et recevez des webhooks signés.
35
+
36
+ - Paquet PyPI : `fameen-messaging` (module `fameen_messaging`) — version 0.1.0
37
+ - Python ≥ 3.9 — dépendance unique : [`httpx`](https://www.python-httpx.org/)
38
+ - Client synchrone (`FameenMessaging`) **et** asynchrone (`AsyncFameenMessaging`)
39
+ - Réessais automatiques (réseau, 429, 5xx idempotents), erreurs typées, vérification de webhooks en temps constant
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install fameen-messaging
45
+ ```
46
+
47
+ ## Démarrage rapide
48
+
49
+ ```python
50
+ import os
51
+ from fameen_messaging import FameenMessaging
52
+
53
+ client = FameenMessaging(api_key=os.environ["FAMEEN_API_KEY"]) # clé "fam_…", jamais en dur
54
+
55
+ # SMS
56
+ message = client.sms.send("+224620000000", "Bonjour {prenom} !")
57
+ print(message.sid, message.status) # msg_… queued
58
+
59
+ # WhatsApp
60
+ client.whatsapp.send("+224620000000", "Votre commande est prête ✅")
61
+
62
+ # Email
63
+ client.email.send("client@exemple.com", "Corps du message", subject="Bienvenue !")
64
+
65
+ # Envoi unifié — canal explicite ou déduit (« @ » dans `to` → email, sinon SMS)
66
+ client.messages.create("client@exemple.com", "Bonjour !", subject="Info")
67
+ client.messages.create("+224620000000", "Bonjour !", channel="whatsapp")
68
+
69
+ # Suivi
70
+ statut = client.messages.get(message.sid)
71
+ page = client.messages.list(channel="sms", status="delivered", page=1, limit=30)
72
+ for m in page.data:
73
+ print(m.sid, m.status, m.delivered_at)
74
+
75
+ # Solde du portefeuille
76
+ solde = client.wallet.balance()
77
+ print(solde.sms_credits, solde.wa_credits, solde.email_credits, solde.billing.mode)
78
+ ```
79
+
80
+ Le contenu accepte les variables de personnalisation `{prenom}`, `{nom}`, `{email}`, `{phone}` (max 5 000 caractères ; `subject` ≤ 255).
81
+
82
+ ## Client asynchrone
83
+
84
+ Mêmes méthodes, en `async`, sur `httpx.AsyncClient` :
85
+
86
+ ```python
87
+ import asyncio
88
+ import os
89
+ from fameen_messaging import AsyncFameenMessaging
90
+
91
+ async def main():
92
+ async with AsyncFameenMessaging(api_key=os.environ["FAMEEN_API_KEY"]) as client:
93
+ message = await client.sms.send("+224620000000", "Bonjour !")
94
+ solde = await client.wallet.balance()
95
+ print(message.sid, solde.sms_credits)
96
+
97
+ asyncio.run(main())
98
+ ```
99
+
100
+ ## Idempotence
101
+
102
+ Passez une `idempotency_key` (en-tête `Idempotency-Key`, fenêtre de 24 h côté serveur) : tout réessai renvoie la réponse d'origine au lieu de créer un doublon — et cela rend les réessais automatiques du SDK **sûrs sur les POST** :
103
+
104
+ ```python
105
+ client.sms.send("+224620000000", "Commande confirmée", idempotency_key="commande-42-confirmation")
106
+ ```
107
+
108
+ ## Erreurs
109
+
110
+ Toutes les erreurs du SDK héritent de `FameenError` :
111
+
112
+ ```python
113
+ from fameen_messaging import FameenAPIError, FameenConnectionError
114
+
115
+ try:
116
+ client.sms.send("+224620000000", "Bonjour !")
117
+ except FameenAPIError as err:
118
+ print(err.status, err.code, err) # ex. 402 insufficient_credits Crédits insuffisants…
119
+ if err.code == "insufficient_credits":
120
+ ... # rechargez le portefeuille
121
+ if err.retry_after is not None:
122
+ ... # 429 : attendez err.retry_after secondes
123
+ except FameenConnectionError:
124
+ ... # l'API n'a pas pu être jointe (DNS, timeout, coupure réseau)
125
+ ```
126
+
127
+ | Exception | Quand | Attributs |
128
+ |---|---|---|
129
+ | `FameenError` | classe mère | `str(err)` = message |
130
+ | `FameenAPIError` | réponse HTTP non-2xx | `status`, `code`, `retry_after`, `rate_limit` |
131
+ | `FameenConnectionError` | API injoignable après épuisement des réessais | — |
132
+ | `WebhookVerificationError` | signature/corps de webhook invalide | — |
133
+
134
+ Codes stables (`err.code`) : `bad_request` (400), `unauthorized` (401), `insufficient_credits` (402), `channel_not_allowed` (403), `not_found` (404), `rate_limited` (429), `internal_error` (5xx), `unknown_error`. Si le corps d'erreur est illisible, le code est déduit du statut HTTP.
135
+
136
+ Une validation locale est faite **avant** tout appel réseau (lève `TypeError`) : `to` et `message` non vides ; `to` contenant « @ » refusé si le canal explicite n'est pas `email`.
137
+
138
+ ## Réessais automatiques
139
+
140
+ `max_retries` = 2 par défaut ; backoff exponentiel `retry_base × 2^tentative + aléa(0..retry_base)` (base 0,5 s).
141
+
142
+ | Situation | Réessayé ? |
143
+ |---|---|
144
+ | Erreur réseau (DNS, timeout, coupure) | ✅ toutes méthodes |
145
+ | HTTP 429 | ✅ en respectant `Retry-After` (secondes) si présent |
146
+ | HTTP 5xx sur **GET** | ✅ |
147
+ | HTTP 5xx sur **POST avec `idempotency_key`** | ✅ |
148
+ | HTTP 5xx sur POST **sans** clé d'idempotence | ❌ (la requête a pu être traitée côté serveur) |
149
+ | HTTP 4xx (400, 401, 402, 403, 404…) | ❌ |
150
+
151
+ ## Limite de débit
152
+
153
+ 60 requêtes/minute/clé. Chaque réponse expose `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (epoch secondes) ; le SDK les mémorise :
154
+
155
+ ```python
156
+ info = client.last_rate_limit # RateLimitInfo(limit=60, remaining=42, reset=1770000000) ou None
157
+ ```
158
+
159
+ ## Webhooks
160
+
161
+ L'API notifie votre `status_callback` à chaque changement de statut (`event` ∈ `queued | sent | delivered | failed`). Chaque requête est signée : **HMAC-SHA256 (hex) du corps brut** avec le secret `whsec_…` du compte, dans l'en-tête `X-Fameen-Signature` (en-tête informatif : `X-Fameen-Event`).
162
+
163
+ ⚠️ Vérifiez toujours la signature sur le **corps brut** (les octets reçus, avant tout parsing JSON). La comparaison est faite en temps constant.
164
+
165
+ ```python
166
+ from fameen_messaging import verify_webhook_signature, construct_webhook_event, WebhookVerificationError
167
+
168
+ ok = verify_webhook_signature(corps_brut, signature, secret) # -> bool
169
+ event = construct_webhook_event(corps_brut, signature, secret) # -> WebhookEvent (vérifie PUIS parse)
170
+ ```
171
+
172
+ ### Django
173
+
174
+ ```python
175
+ # views.py
176
+ import os
177
+ from django.http import HttpResponse
178
+ from django.views.decorators.csrf import csrf_exempt
179
+ from fameen_messaging import construct_webhook_event, WebhookVerificationError
180
+
181
+ @csrf_exempt
182
+ def fameen_webhook(request):
183
+ try:
184
+ event = construct_webhook_event(
185
+ request.body, # corps brut : request.body, PAS request.POST
186
+ request.headers.get("X-Fameen-Signature"),
187
+ os.environ["FAMEEN_WEBHOOK_SECRET"],
188
+ )
189
+ except WebhookVerificationError:
190
+ return HttpResponse(status=401)
191
+
192
+ if event.event == "delivered":
193
+ ... # mettez à jour votre base avec event.sid
194
+ return HttpResponse(status=200)
195
+ ```
196
+
197
+ ### Flask
198
+
199
+ ```python
200
+ import os
201
+ from flask import Flask, request
202
+ from fameen_messaging import construct_webhook_event, WebhookVerificationError
203
+
204
+ app = Flask(__name__)
205
+
206
+ @app.post("/webhooks/fameen")
207
+ def fameen_webhook():
208
+ try:
209
+ event = construct_webhook_event(
210
+ request.get_data(), # corps brut : get_data(), PAS request.json
211
+ request.headers.get("X-Fameen-Signature"),
212
+ os.environ["FAMEEN_WEBHOOK_SECRET"],
213
+ )
214
+ except WebhookVerificationError:
215
+ return "", 401
216
+
217
+ print(event.sid, event.status)
218
+ return "", 200
219
+ ```
220
+
221
+ ### FastAPI
222
+
223
+ ```python
224
+ import os
225
+ from fastapi import FastAPI, Request, Response
226
+ from fameen_messaging import construct_webhook_event, WebhookVerificationError
227
+
228
+ app = FastAPI()
229
+
230
+ @app.post("/webhooks/fameen")
231
+ async def fameen_webhook(request: Request):
232
+ payload = await request.body() # corps brut, avant tout parsing
233
+ try:
234
+ event = construct_webhook_event(
235
+ payload,
236
+ request.headers.get("x-fameen-signature"),
237
+ os.environ["FAMEEN_WEBHOOK_SECRET"],
238
+ )
239
+ except WebhookVerificationError:
240
+ return Response(status_code=401)
241
+
242
+ print(event.sid, event.status)
243
+ return Response(status_code=200)
244
+ ```
245
+
246
+ ## Configuration
247
+
248
+ ```python
249
+ FameenMessaging(
250
+ api_key="fam_…", # requis
251
+ base_url="https://business.fameengroupe.com/api/v1", # défaut ; « / » finaux retirés
252
+ timeout=30.0, # timeout httpx par tentative, en secondes
253
+ max_retries=2, # réessais automatiques
254
+ retry_base=0.5, # base du backoff exponentiel (s) — utile en test
255
+ transport=None, # transport httpx injectable (httpx.MockTransport en test)
256
+ )
257
+ ```
258
+
259
+ `AsyncFameenMessaging` accepte exactement les mêmes options.
260
+
261
+ ## Modèles de données
262
+
263
+ Les retours sont des **dataclasses gelées**, construites de manière tolérante depuis le JSON (champs inconnus ignorés, champs manquants → `None`/`0`). Les clés camelCase deviennent snake_case (`externalId` → `external_id`) ; la clé `from` (mot réservé Python) devient `from_`.
264
+
265
+ | Dataclass | Renvoyée par |
266
+ |---|---|
267
+ | `MessageResource` | `sms/whatsapp/email.send`, `messages.create`, `messages.get` |
268
+ | `MessageList` | `messages.list` (`.data` = liste de `MessageResource`) |
269
+ | `WalletBalance` (+ `WalletBilling`) | `wallet.balance` |
270
+ | `WebhookEvent` | `construct_webhook_event` |
271
+ | `RateLimitInfo` | `client.last_rate_limit`, `err.rate_limit` |
272
+
273
+ `messages.history()` est **déprécié** (lignes brutes, `DeprecationWarning`) — préférez `messages.list()`.
274
+
275
+ ## Tests (développement)
276
+
277
+ ```bash
278
+ python -m venv .venv
279
+ .venv/Scripts/python -m pip install httpx pytest pytest-asyncio # (Linux/macOS : .venv/bin/python)
280
+ .venv/Scripts/python -m pytest -q
281
+ ```
282
+
283
+ Les tests utilisent `httpx.MockTransport` : aucun appel réseau.
284
+
285
+ ## Licence
286
+
287
+ MIT — © Fameen Groupe.
@@ -0,0 +1,256 @@
1
+ # Fameen Messaging — SDK Python officiel
2
+
3
+ SDK Python officiel de l'API **Fameen Messaging** : envoyez des **SMS**, des messages **WhatsApp** et des **emails** depuis vos applications Python, suivez leur statut et recevez des webhooks signés.
4
+
5
+ - Paquet PyPI : `fameen-messaging` (module `fameen_messaging`) — version 0.1.0
6
+ - Python ≥ 3.9 — dépendance unique : [`httpx`](https://www.python-httpx.org/)
7
+ - Client synchrone (`FameenMessaging`) **et** asynchrone (`AsyncFameenMessaging`)
8
+ - Réessais automatiques (réseau, 429, 5xx idempotents), erreurs typées, vérification de webhooks en temps constant
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install fameen-messaging
14
+ ```
15
+
16
+ ## Démarrage rapide
17
+
18
+ ```python
19
+ import os
20
+ from fameen_messaging import FameenMessaging
21
+
22
+ client = FameenMessaging(api_key=os.environ["FAMEEN_API_KEY"]) # clé "fam_…", jamais en dur
23
+
24
+ # SMS
25
+ message = client.sms.send("+224620000000", "Bonjour {prenom} !")
26
+ print(message.sid, message.status) # msg_… queued
27
+
28
+ # WhatsApp
29
+ client.whatsapp.send("+224620000000", "Votre commande est prête ✅")
30
+
31
+ # Email
32
+ client.email.send("client@exemple.com", "Corps du message", subject="Bienvenue !")
33
+
34
+ # Envoi unifié — canal explicite ou déduit (« @ » dans `to` → email, sinon SMS)
35
+ client.messages.create("client@exemple.com", "Bonjour !", subject="Info")
36
+ client.messages.create("+224620000000", "Bonjour !", channel="whatsapp")
37
+
38
+ # Suivi
39
+ statut = client.messages.get(message.sid)
40
+ page = client.messages.list(channel="sms", status="delivered", page=1, limit=30)
41
+ for m in page.data:
42
+ print(m.sid, m.status, m.delivered_at)
43
+
44
+ # Solde du portefeuille
45
+ solde = client.wallet.balance()
46
+ print(solde.sms_credits, solde.wa_credits, solde.email_credits, solde.billing.mode)
47
+ ```
48
+
49
+ Le contenu accepte les variables de personnalisation `{prenom}`, `{nom}`, `{email}`, `{phone}` (max 5 000 caractères ; `subject` ≤ 255).
50
+
51
+ ## Client asynchrone
52
+
53
+ Mêmes méthodes, en `async`, sur `httpx.AsyncClient` :
54
+
55
+ ```python
56
+ import asyncio
57
+ import os
58
+ from fameen_messaging import AsyncFameenMessaging
59
+
60
+ async def main():
61
+ async with AsyncFameenMessaging(api_key=os.environ["FAMEEN_API_KEY"]) as client:
62
+ message = await client.sms.send("+224620000000", "Bonjour !")
63
+ solde = await client.wallet.balance()
64
+ print(message.sid, solde.sms_credits)
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ ## Idempotence
70
+
71
+ Passez une `idempotency_key` (en-tête `Idempotency-Key`, fenêtre de 24 h côté serveur) : tout réessai renvoie la réponse d'origine au lieu de créer un doublon — et cela rend les réessais automatiques du SDK **sûrs sur les POST** :
72
+
73
+ ```python
74
+ client.sms.send("+224620000000", "Commande confirmée", idempotency_key="commande-42-confirmation")
75
+ ```
76
+
77
+ ## Erreurs
78
+
79
+ Toutes les erreurs du SDK héritent de `FameenError` :
80
+
81
+ ```python
82
+ from fameen_messaging import FameenAPIError, FameenConnectionError
83
+
84
+ try:
85
+ client.sms.send("+224620000000", "Bonjour !")
86
+ except FameenAPIError as err:
87
+ print(err.status, err.code, err) # ex. 402 insufficient_credits Crédits insuffisants…
88
+ if err.code == "insufficient_credits":
89
+ ... # rechargez le portefeuille
90
+ if err.retry_after is not None:
91
+ ... # 429 : attendez err.retry_after secondes
92
+ except FameenConnectionError:
93
+ ... # l'API n'a pas pu être jointe (DNS, timeout, coupure réseau)
94
+ ```
95
+
96
+ | Exception | Quand | Attributs |
97
+ |---|---|---|
98
+ | `FameenError` | classe mère | `str(err)` = message |
99
+ | `FameenAPIError` | réponse HTTP non-2xx | `status`, `code`, `retry_after`, `rate_limit` |
100
+ | `FameenConnectionError` | API injoignable après épuisement des réessais | — |
101
+ | `WebhookVerificationError` | signature/corps de webhook invalide | — |
102
+
103
+ Codes stables (`err.code`) : `bad_request` (400), `unauthorized` (401), `insufficient_credits` (402), `channel_not_allowed` (403), `not_found` (404), `rate_limited` (429), `internal_error` (5xx), `unknown_error`. Si le corps d'erreur est illisible, le code est déduit du statut HTTP.
104
+
105
+ Une validation locale est faite **avant** tout appel réseau (lève `TypeError`) : `to` et `message` non vides ; `to` contenant « @ » refusé si le canal explicite n'est pas `email`.
106
+
107
+ ## Réessais automatiques
108
+
109
+ `max_retries` = 2 par défaut ; backoff exponentiel `retry_base × 2^tentative + aléa(0..retry_base)` (base 0,5 s).
110
+
111
+ | Situation | Réessayé ? |
112
+ |---|---|
113
+ | Erreur réseau (DNS, timeout, coupure) | ✅ toutes méthodes |
114
+ | HTTP 429 | ✅ en respectant `Retry-After` (secondes) si présent |
115
+ | HTTP 5xx sur **GET** | ✅ |
116
+ | HTTP 5xx sur **POST avec `idempotency_key`** | ✅ |
117
+ | HTTP 5xx sur POST **sans** clé d'idempotence | ❌ (la requête a pu être traitée côté serveur) |
118
+ | HTTP 4xx (400, 401, 402, 403, 404…) | ❌ |
119
+
120
+ ## Limite de débit
121
+
122
+ 60 requêtes/minute/clé. Chaque réponse expose `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (epoch secondes) ; le SDK les mémorise :
123
+
124
+ ```python
125
+ info = client.last_rate_limit # RateLimitInfo(limit=60, remaining=42, reset=1770000000) ou None
126
+ ```
127
+
128
+ ## Webhooks
129
+
130
+ L'API notifie votre `status_callback` à chaque changement de statut (`event` ∈ `queued | sent | delivered | failed`). Chaque requête est signée : **HMAC-SHA256 (hex) du corps brut** avec le secret `whsec_…` du compte, dans l'en-tête `X-Fameen-Signature` (en-tête informatif : `X-Fameen-Event`).
131
+
132
+ ⚠️ Vérifiez toujours la signature sur le **corps brut** (les octets reçus, avant tout parsing JSON). La comparaison est faite en temps constant.
133
+
134
+ ```python
135
+ from fameen_messaging import verify_webhook_signature, construct_webhook_event, WebhookVerificationError
136
+
137
+ ok = verify_webhook_signature(corps_brut, signature, secret) # -> bool
138
+ event = construct_webhook_event(corps_brut, signature, secret) # -> WebhookEvent (vérifie PUIS parse)
139
+ ```
140
+
141
+ ### Django
142
+
143
+ ```python
144
+ # views.py
145
+ import os
146
+ from django.http import HttpResponse
147
+ from django.views.decorators.csrf import csrf_exempt
148
+ from fameen_messaging import construct_webhook_event, WebhookVerificationError
149
+
150
+ @csrf_exempt
151
+ def fameen_webhook(request):
152
+ try:
153
+ event = construct_webhook_event(
154
+ request.body, # corps brut : request.body, PAS request.POST
155
+ request.headers.get("X-Fameen-Signature"),
156
+ os.environ["FAMEEN_WEBHOOK_SECRET"],
157
+ )
158
+ except WebhookVerificationError:
159
+ return HttpResponse(status=401)
160
+
161
+ if event.event == "delivered":
162
+ ... # mettez à jour votre base avec event.sid
163
+ return HttpResponse(status=200)
164
+ ```
165
+
166
+ ### Flask
167
+
168
+ ```python
169
+ import os
170
+ from flask import Flask, request
171
+ from fameen_messaging import construct_webhook_event, WebhookVerificationError
172
+
173
+ app = Flask(__name__)
174
+
175
+ @app.post("/webhooks/fameen")
176
+ def fameen_webhook():
177
+ try:
178
+ event = construct_webhook_event(
179
+ request.get_data(), # corps brut : get_data(), PAS request.json
180
+ request.headers.get("X-Fameen-Signature"),
181
+ os.environ["FAMEEN_WEBHOOK_SECRET"],
182
+ )
183
+ except WebhookVerificationError:
184
+ return "", 401
185
+
186
+ print(event.sid, event.status)
187
+ return "", 200
188
+ ```
189
+
190
+ ### FastAPI
191
+
192
+ ```python
193
+ import os
194
+ from fastapi import FastAPI, Request, Response
195
+ from fameen_messaging import construct_webhook_event, WebhookVerificationError
196
+
197
+ app = FastAPI()
198
+
199
+ @app.post("/webhooks/fameen")
200
+ async def fameen_webhook(request: Request):
201
+ payload = await request.body() # corps brut, avant tout parsing
202
+ try:
203
+ event = construct_webhook_event(
204
+ payload,
205
+ request.headers.get("x-fameen-signature"),
206
+ os.environ["FAMEEN_WEBHOOK_SECRET"],
207
+ )
208
+ except WebhookVerificationError:
209
+ return Response(status_code=401)
210
+
211
+ print(event.sid, event.status)
212
+ return Response(status_code=200)
213
+ ```
214
+
215
+ ## Configuration
216
+
217
+ ```python
218
+ FameenMessaging(
219
+ api_key="fam_…", # requis
220
+ base_url="https://business.fameengroupe.com/api/v1", # défaut ; « / » finaux retirés
221
+ timeout=30.0, # timeout httpx par tentative, en secondes
222
+ max_retries=2, # réessais automatiques
223
+ retry_base=0.5, # base du backoff exponentiel (s) — utile en test
224
+ transport=None, # transport httpx injectable (httpx.MockTransport en test)
225
+ )
226
+ ```
227
+
228
+ `AsyncFameenMessaging` accepte exactement les mêmes options.
229
+
230
+ ## Modèles de données
231
+
232
+ Les retours sont des **dataclasses gelées**, construites de manière tolérante depuis le JSON (champs inconnus ignorés, champs manquants → `None`/`0`). Les clés camelCase deviennent snake_case (`externalId` → `external_id`) ; la clé `from` (mot réservé Python) devient `from_`.
233
+
234
+ | Dataclass | Renvoyée par |
235
+ |---|---|
236
+ | `MessageResource` | `sms/whatsapp/email.send`, `messages.create`, `messages.get` |
237
+ | `MessageList` | `messages.list` (`.data` = liste de `MessageResource`) |
238
+ | `WalletBalance` (+ `WalletBilling`) | `wallet.balance` |
239
+ | `WebhookEvent` | `construct_webhook_event` |
240
+ | `RateLimitInfo` | `client.last_rate_limit`, `err.rate_limit` |
241
+
242
+ `messages.history()` est **déprécié** (lignes brutes, `DeprecationWarning`) — préférez `messages.list()`.
243
+
244
+ ## Tests (développement)
245
+
246
+ ```bash
247
+ python -m venv .venv
248
+ .venv/Scripts/python -m pip install httpx pytest pytest-asyncio # (Linux/macOS : .venv/bin/python)
249
+ .venv/Scripts/python -m pytest -q
250
+ ```
251
+
252
+ Les tests utilisent `httpx.MockTransport` : aucun appel réseau.
253
+
254
+ ## Licence
255
+
256
+ MIT — © Fameen Groupe.
@@ -0,0 +1,65 @@
1
+ """SDK Python officiel de l'API Fameen Messaging (SMS, WhatsApp, Email).
2
+
3
+ Exemple minimal::
4
+
5
+ from fameen_messaging import FameenMessaging
6
+
7
+ client = FameenMessaging(api_key="fam_…")
8
+ message = client.sms.send("+224620000000", "Bonjour {prenom} !")
9
+ print(message.sid, message.status)
10
+
11
+ Webhooks::
12
+
13
+ from fameen_messaging import construct_webhook_event
14
+
15
+ event = construct_webhook_event(corps_brut, signature, secret)
16
+ """
17
+
18
+ from .client import (
19
+ DEFAULT_BASE_URL,
20
+ VERSION,
21
+ AsyncFameenMessaging,
22
+ FameenMessaging,
23
+ )
24
+ from .errors import (
25
+ FameenAPIError,
26
+ FameenConnectionError,
27
+ FameenError,
28
+ WebhookVerificationError,
29
+ )
30
+ from .types import (
31
+ MessageList,
32
+ MessageResource,
33
+ RateLimitInfo,
34
+ WalletBalance,
35
+ WalletBilling,
36
+ WebhookEvent,
37
+ )
38
+ from .webhooks import construct_webhook_event, verify_webhook_signature
39
+
40
+ __version__ = VERSION
41
+
42
+ __all__ = [
43
+ # Clients
44
+ "FameenMessaging",
45
+ "AsyncFameenMessaging",
46
+ "DEFAULT_BASE_URL",
47
+ # Erreurs
48
+ "FameenError",
49
+ "FameenAPIError",
50
+ "FameenConnectionError",
51
+ "WebhookVerificationError",
52
+ # Modèles
53
+ "MessageResource",
54
+ "MessageList",
55
+ "WalletBalance",
56
+ "WalletBilling",
57
+ "WebhookEvent",
58
+ "RateLimitInfo",
59
+ # Webhooks
60
+ "verify_webhook_signature",
61
+ "construct_webhook_event",
62
+ # Divers
63
+ "VERSION",
64
+ "__version__",
65
+ ]