senndo 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,44 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Builds
6
+ dist/
7
+ build/
8
+ .svelte-kit/
9
+ *.tsbuildinfo
10
+
11
+ # Environnement — les secrets ne rentrent JAMAIS dans ce repo
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+
16
+ # Docs privés (credentials, drafts hérités) — jamais committés
17
+ private_docs/
18
+
19
+ # Screenshots de vérification (générés par beat, lus localement par le verifier)
20
+ artifacts/
21
+
22
+ # PID des serveurs e2e (make app-up / app-down)
23
+ .pids/
24
+
25
+ # Divers
26
+ .DS_Store
27
+ *.log
28
+ coverage/
29
+ playwright-report/
30
+ test-results/
31
+
32
+ # SDK Python — environnement virtuel local et artefacts d'empaquetage
33
+ packages/sdk-python/.venv/
34
+ packages/sdk-python/dist/
35
+ packages/sdk-python/**/__pycache__/
36
+ packages/sdk-python/**/*.egg-info/
37
+ packages/sdk-python/.mypy_cache/
38
+ packages/sdk-python/.pytest_cache/
39
+
40
+ # SDK PHP — dépendances Composer (require-dev seulement : le paquet publié n'en a aucune)
41
+ packages/sdk-php/vendor/
42
+ packages/sdk-php/composer.lock
43
+ packages/sdk-php/.phpunit.cache/
44
+ packages/sdk-php/.phpunit.result.cache
@@ -0,0 +1,26 @@
1
+ # Journal des versions — `senndo` (Python)
2
+
3
+ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le versionnage
4
+ sémantique.
5
+
6
+ ## 0.1.0 — 2026-08-02
7
+
8
+ Première publication. La version reste `0.x` tant que la surface n'a pas été exercée par des
9
+ intégrations réelles : un `1.0.0` promet une stabilité que rien n'a encore éprouvée.
10
+
11
+ ### Ajouté
12
+
13
+ - `SenndoClient` — les 20 opérations de l'API publique, une méthode par opération, nommées en
14
+ snake_case depuis les identifiants du contrat.
15
+ - Types générés depuis le contrat OpenAPI de senndo : corps, paramètres de requête et réponses en
16
+ `TypedDict`, énumérations en `Literal`.
17
+ - Erreurs typées par famille (`SenndoInsufficientFundsError`, `SenndoRateLimitError`,
18
+ `SenndoValidationError`…) — on branche sur une classe ou sur `error.code`, jamais sur un message.
19
+ - Idempotence de première classe : `idempotencyKey` obligatoire sur l'envoi, préfixes réservés
20
+ refusés localement, `new_idempotency_key()` pour les cas sans clé métier.
21
+ - Retentatives limitées à ce qui est rejouable : `GET`/`DELETE` et les `POST` porteurs d'une clé
22
+ d'idempotence, sur échec de transport, 429 et 5xx uniquement.
23
+ - Délais explicites (30 s par défaut), surchargeables par appel via `RequestOptions`.
24
+ - Transport injectable (`types.Transport`) — `urllib.request` par défaut, aucune dépendance.
25
+ - Décodage `parse_float=Decimal` : aucun nombre à virgule ne traverse un flottant.
26
+ - Clé API masquée dans `repr()` et `str()`.
senndo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 senndo
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.
senndo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,287 @@
1
+ Metadata-Version: 2.4
2
+ Name: senndo
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the senndo messaging and verification API.
5
+ Project-URL: Homepage, https://senndo.com
6
+ Project-URL: Documentation, https://senndo.com/developpeurs
7
+ Project-URL: Changelog, https://github.com/senndo/senndo-python/blob/main/CHANGELOG.md
8
+ Project-URL: Source, https://github.com/senndo/senndo-python
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 senndo
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: cpaas,email,messaging,otp,senndo,sms,voice,whatsapp
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Communications
40
+ Classifier: Typing :: Typed
41
+ Requires-Python: >=3.11
42
+ Description-Content-Type: text/markdown
43
+
44
+ # senndo — SDK Python officiel
45
+
46
+ Messagerie multicanale et vérification : SMS, WhatsApp, e-mail, voix, OTP. Une seule API, un seul
47
+ solde, un verdict de livraison par message.
48
+
49
+ ```bash
50
+ uv add senndo
51
+ pip install senndo
52
+ poetry add senndo
53
+ ```
54
+
55
+ Python ≥ 3.11. **Aucune dépendance d'exécution.**
56
+
57
+ ---
58
+
59
+ ## Premier envoi
60
+
61
+ ```python
62
+ senndo = SenndoClient(api_key=cle_api)
63
+
64
+ envoi = senndo.send_message(
65
+ {
66
+ "channel": "sms",
67
+ "to": "+33612345678",
68
+ "text": "Votre code de connexion est 4821.",
69
+ "idempotencyKey": f"connexion-{utilisateur_id}",
70
+ }
71
+ )
72
+
73
+ print(envoi["id"], envoi["status"])
74
+ ```
75
+
76
+ `send_message` rend l'état **au moment de l'acceptation**, pas le verdict final. Un `sent` dit que
77
+ l'opérateur a pris le message ; il ne dit pas qu'il est arrivé. Le verdict se lit sur un webhook, ou
78
+ en relisant le message.
79
+
80
+ ```python
81
+ message = senndo.get_message(identifiant_du_message)
82
+
83
+ if message["status"] == "failed":
84
+ journaliser("échec", message.get("failureCode"))
85
+ ```
86
+
87
+ ---
88
+
89
+ ## La clé d'idempotence : le SDK n'en fabrique pas à votre place
90
+
91
+ `idempotencyKey` est **obligatoire** sur tout envoi, et c'est délibéré. Rejouer la même clé renvoie
92
+ le message déjà créé — sans jamais redébiter le compte.
93
+
94
+ Le SDK **n'en pose jamais une pour vous**. Une clé inventée au moment de l'appel serait perdue si le
95
+ processus meurt entre l'envoi et la réponse : exactement le cas où l'idempotence sert. La bonne clé
96
+ vient de votre domaine.
97
+
98
+ ```python
99
+ # CORRECT : la clé survit à un redémarrage, parce qu'elle vient de votre base.
100
+ senndo.send_message(
101
+ {
102
+ "channel": "whatsapp_cloud",
103
+ "to": commande["telephone"],
104
+ "text": "Votre commande est prête.",
105
+ "idempotencyKey": f"commande-{commande['reference']}-prete",
106
+ }
107
+ )
108
+ ```
109
+
110
+ `new_idempotency_key()` existe pour les cas où il n'y a **vraiment** rien à dériver — un envoi
111
+ manuel depuis un script, un test :
112
+
113
+ ```python
114
+ senndo.send_message(
115
+ {
116
+ "channel": "sms",
117
+ "to": "+15551234567",
118
+ "text": "Essai.",
119
+ "idempotencyKey": new_idempotency_key("essai-"),
120
+ }
121
+ )
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Les erreurs se branchent sur une classe, jamais sur un message
127
+
128
+ ```python
129
+ try:
130
+ senndo.send_message(
131
+ {
132
+ "channel": "sms",
133
+ "to": "+22507000000",
134
+ "text": "Bonjour.",
135
+ "idempotencyKey": f"bienvenue-{utilisateur_id}",
136
+ }
137
+ )
138
+ except SenndoInsufficientFundsError:
139
+ recharger_le_compte()
140
+ except SenndoValidationError as erreur:
141
+ journaliser("appel à corriger", erreur.code, erreur.api_message)
142
+ except SenndoRateLimitError as erreur:
143
+ attendre(erreur.retry_after or 5)
144
+ except SenndoError as erreur:
145
+ journaliser("échec senndo", erreur)
146
+ ```
147
+
148
+ `erreur.code` est **stable** ; `erreur.api_message` est un libellé humain qui évolue. Ne branchez
149
+ jamais sur le second.
150
+
151
+ Le code d'échec d'un message livré-puis-refusé est en union **ouverte** : senndo ajoute des valeurs,
152
+ n'en retire pas.
153
+
154
+ ```python
155
+ message = senndo.get_message(identifiant_du_message)
156
+ code = message.get("failureCode")
157
+
158
+ if code is not None and code not in KNOWN_FAILURE_CODES:
159
+ journaliser("code plus récent que ce SDK", code)
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Les montants sont des chaînes décimales
165
+
166
+ Un `NUMERIC(18,6)` passé par un flottant perd des unités sur les longues traînes, et un prix
167
+ unitaire sub-centime arrondi à deux décimales devient zéro.
168
+
169
+ ```python
170
+ solde = senndo.get_balance({"currency": "EUR"})
171
+ disponible = Decimal(solde["balanceUsd"])
172
+
173
+ journal = senndo.list_ledger({"pageSize": 100})
174
+ mouvement_net = sum(
175
+ (Decimal(ligne["amountUsd"]) for ligne in journal["rows"]),
176
+ Decimal("0"),
177
+ )
178
+ ```
179
+
180
+ Le SDK décode les réponses avec `parse_float=Decimal` : aucun nombre à virgule ne traverse un
181
+ `float`.
182
+
183
+ ---
184
+
185
+ ## Retentatives : ce qui est rejoué, et ce qui ne l'est jamais
186
+
187
+ Le SDK retente **uniquement** ce qui peut l'être sans conséquence :
188
+
189
+ | Appel | Retenté ? |
190
+ |---|---|
191
+ | `GET`, `DELETE` | oui — sur échec de transport, 429, 5xx |
192
+ | `send_message` (porte une clé d'idempotence) | oui |
193
+ | `create_webhook`, `estimate_message`, `revoke_webhook` | **jamais** |
194
+ | tout `4xx` autre que 429 | jamais |
195
+
196
+ `create_webhook` crée une ressource à chaque exécution : une retentative aveugle produirait deux
197
+ endpoints, donc deux livraisons pour chaque événement.
198
+
199
+ ```python
200
+ options = RequestOptions(timeout=10.0, max_retries=0)
201
+ senndo.send_message(
202
+ {
203
+ "channel": "sms",
204
+ "to": "+5511998877665",
205
+ "text": "Ping.",
206
+ "idempotencyKey": f"ping-{tentative}",
207
+ },
208
+ options,
209
+ )
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Téléverser un média
215
+
216
+ ```python
217
+ fichier = senndo.upload_media(
218
+ MultipartUpload(file=octets, file_name="facture.pdf", content_type="application/pdf")
219
+ )
220
+
221
+ senndo.send_message(
222
+ {
223
+ "channel": "whatsapp_cloud",
224
+ "to": "+33612345678",
225
+ "text": "Votre facture.",
226
+ "media": {"ref": fichier["ref"]},
227
+ "idempotencyKey": f"facture-{commande['reference']}",
228
+ }
229
+ )
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Brancher votre propre client HTTP
235
+
236
+ Le transport par défaut est `urllib.request` — zéro dépendance. Un projet qui a déjà `httpx`,
237
+ `requests`, un proxy d'entreprise ou du mTLS injecte le sien, et garde la validation, les erreurs
238
+ typées et la politique de retentative.
239
+
240
+ ```python
241
+ def transport_maison(requete: HttpRequest) -> HttpResponse:
242
+ reponse = appeler_mon_client(
243
+ requete.method, requete.url, dict(requete.headers), requete.body, requete.timeout
244
+ )
245
+ return HttpResponse(status=reponse.code, headers=reponse.entetes, body=reponse.texte)
246
+
247
+
248
+ senndo = SenndoClient(api_key=cle_api, transport=transport_maison)
249
+ ```
250
+
251
+ Le transport doit lever `TimeoutError` sur dépassement de délai et `OSError` sur échec de transport,
252
+ et **ne jamais lever** sur un statut d'erreur HTTP — sinon le code stable de l'enveloppe est perdu.
253
+
254
+ ---
255
+
256
+ ## La clé API ne s'imprime pas
257
+
258
+ ```python
259
+ journaliser(repr(senndo)) # SenndoClient(base_url='…', api_key='sk_live_…32 caractères masqués')
260
+ ```
261
+
262
+ Il n'existe aucun accesseur qui rende la clé en clair.
263
+
264
+ ---
265
+
266
+ ## Webhooks
267
+
268
+ ```python
269
+ endpoint = senndo.create_webhook(
270
+ {
271
+ "name": "Production",
272
+ "url": "https://exemple.test/senndo",
273
+ "events": ["message.sent", "message.failed"],
274
+ }
275
+ )
276
+
277
+ conserver_le_secret(endpoint["secret"])
278
+ ```
279
+
280
+ Le secret n'est lisible **qu'à la création**. Il signe chaque livraison : vérifiez la signature
281
+ avant de faire quoi que ce soit du corps.
282
+
283
+ ---
284
+
285
+ ## Licence
286
+
287
+ MIT. Voir [CHANGELOG.md](CHANGELOG.md) pour les changements de version.
senndo-0.1.0/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # senndo — SDK Python officiel
2
+
3
+ Messagerie multicanale et vérification : SMS, WhatsApp, e-mail, voix, OTP. Une seule API, un seul
4
+ solde, un verdict de livraison par message.
5
+
6
+ ```bash
7
+ uv add senndo
8
+ pip install senndo
9
+ poetry add senndo
10
+ ```
11
+
12
+ Python ≥ 3.11. **Aucune dépendance d'exécution.**
13
+
14
+ ---
15
+
16
+ ## Premier envoi
17
+
18
+ ```python
19
+ senndo = SenndoClient(api_key=cle_api)
20
+
21
+ envoi = senndo.send_message(
22
+ {
23
+ "channel": "sms",
24
+ "to": "+33612345678",
25
+ "text": "Votre code de connexion est 4821.",
26
+ "idempotencyKey": f"connexion-{utilisateur_id}",
27
+ }
28
+ )
29
+
30
+ print(envoi["id"], envoi["status"])
31
+ ```
32
+
33
+ `send_message` rend l'état **au moment de l'acceptation**, pas le verdict final. Un `sent` dit que
34
+ l'opérateur a pris le message ; il ne dit pas qu'il est arrivé. Le verdict se lit sur un webhook, ou
35
+ en relisant le message.
36
+
37
+ ```python
38
+ message = senndo.get_message(identifiant_du_message)
39
+
40
+ if message["status"] == "failed":
41
+ journaliser("échec", message.get("failureCode"))
42
+ ```
43
+
44
+ ---
45
+
46
+ ## La clé d'idempotence : le SDK n'en fabrique pas à votre place
47
+
48
+ `idempotencyKey` est **obligatoire** sur tout envoi, et c'est délibéré. Rejouer la même clé renvoie
49
+ le message déjà créé — sans jamais redébiter le compte.
50
+
51
+ Le SDK **n'en pose jamais une pour vous**. Une clé inventée au moment de l'appel serait perdue si le
52
+ processus meurt entre l'envoi et la réponse : exactement le cas où l'idempotence sert. La bonne clé
53
+ vient de votre domaine.
54
+
55
+ ```python
56
+ # CORRECT : la clé survit à un redémarrage, parce qu'elle vient de votre base.
57
+ senndo.send_message(
58
+ {
59
+ "channel": "whatsapp_cloud",
60
+ "to": commande["telephone"],
61
+ "text": "Votre commande est prête.",
62
+ "idempotencyKey": f"commande-{commande['reference']}-prete",
63
+ }
64
+ )
65
+ ```
66
+
67
+ `new_idempotency_key()` existe pour les cas où il n'y a **vraiment** rien à dériver — un envoi
68
+ manuel depuis un script, un test :
69
+
70
+ ```python
71
+ senndo.send_message(
72
+ {
73
+ "channel": "sms",
74
+ "to": "+15551234567",
75
+ "text": "Essai.",
76
+ "idempotencyKey": new_idempotency_key("essai-"),
77
+ }
78
+ )
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Les erreurs se branchent sur une classe, jamais sur un message
84
+
85
+ ```python
86
+ try:
87
+ senndo.send_message(
88
+ {
89
+ "channel": "sms",
90
+ "to": "+22507000000",
91
+ "text": "Bonjour.",
92
+ "idempotencyKey": f"bienvenue-{utilisateur_id}",
93
+ }
94
+ )
95
+ except SenndoInsufficientFundsError:
96
+ recharger_le_compte()
97
+ except SenndoValidationError as erreur:
98
+ journaliser("appel à corriger", erreur.code, erreur.api_message)
99
+ except SenndoRateLimitError as erreur:
100
+ attendre(erreur.retry_after or 5)
101
+ except SenndoError as erreur:
102
+ journaliser("échec senndo", erreur)
103
+ ```
104
+
105
+ `erreur.code` est **stable** ; `erreur.api_message` est un libellé humain qui évolue. Ne branchez
106
+ jamais sur le second.
107
+
108
+ Le code d'échec d'un message livré-puis-refusé est en union **ouverte** : senndo ajoute des valeurs,
109
+ n'en retire pas.
110
+
111
+ ```python
112
+ message = senndo.get_message(identifiant_du_message)
113
+ code = message.get("failureCode")
114
+
115
+ if code is not None and code not in KNOWN_FAILURE_CODES:
116
+ journaliser("code plus récent que ce SDK", code)
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Les montants sont des chaînes décimales
122
+
123
+ Un `NUMERIC(18,6)` passé par un flottant perd des unités sur les longues traînes, et un prix
124
+ unitaire sub-centime arrondi à deux décimales devient zéro.
125
+
126
+ ```python
127
+ solde = senndo.get_balance({"currency": "EUR"})
128
+ disponible = Decimal(solde["balanceUsd"])
129
+
130
+ journal = senndo.list_ledger({"pageSize": 100})
131
+ mouvement_net = sum(
132
+ (Decimal(ligne["amountUsd"]) for ligne in journal["rows"]),
133
+ Decimal("0"),
134
+ )
135
+ ```
136
+
137
+ Le SDK décode les réponses avec `parse_float=Decimal` : aucun nombre à virgule ne traverse un
138
+ `float`.
139
+
140
+ ---
141
+
142
+ ## Retentatives : ce qui est rejoué, et ce qui ne l'est jamais
143
+
144
+ Le SDK retente **uniquement** ce qui peut l'être sans conséquence :
145
+
146
+ | Appel | Retenté ? |
147
+ |---|---|
148
+ | `GET`, `DELETE` | oui — sur échec de transport, 429, 5xx |
149
+ | `send_message` (porte une clé d'idempotence) | oui |
150
+ | `create_webhook`, `estimate_message`, `revoke_webhook` | **jamais** |
151
+ | tout `4xx` autre que 429 | jamais |
152
+
153
+ `create_webhook` crée une ressource à chaque exécution : une retentative aveugle produirait deux
154
+ endpoints, donc deux livraisons pour chaque événement.
155
+
156
+ ```python
157
+ options = RequestOptions(timeout=10.0, max_retries=0)
158
+ senndo.send_message(
159
+ {
160
+ "channel": "sms",
161
+ "to": "+5511998877665",
162
+ "text": "Ping.",
163
+ "idempotencyKey": f"ping-{tentative}",
164
+ },
165
+ options,
166
+ )
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Téléverser un média
172
+
173
+ ```python
174
+ fichier = senndo.upload_media(
175
+ MultipartUpload(file=octets, file_name="facture.pdf", content_type="application/pdf")
176
+ )
177
+
178
+ senndo.send_message(
179
+ {
180
+ "channel": "whatsapp_cloud",
181
+ "to": "+33612345678",
182
+ "text": "Votre facture.",
183
+ "media": {"ref": fichier["ref"]},
184
+ "idempotencyKey": f"facture-{commande['reference']}",
185
+ }
186
+ )
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Brancher votre propre client HTTP
192
+
193
+ Le transport par défaut est `urllib.request` — zéro dépendance. Un projet qui a déjà `httpx`,
194
+ `requests`, un proxy d'entreprise ou du mTLS injecte le sien, et garde la validation, les erreurs
195
+ typées et la politique de retentative.
196
+
197
+ ```python
198
+ def transport_maison(requete: HttpRequest) -> HttpResponse:
199
+ reponse = appeler_mon_client(
200
+ requete.method, requete.url, dict(requete.headers), requete.body, requete.timeout
201
+ )
202
+ return HttpResponse(status=reponse.code, headers=reponse.entetes, body=reponse.texte)
203
+
204
+
205
+ senndo = SenndoClient(api_key=cle_api, transport=transport_maison)
206
+ ```
207
+
208
+ Le transport doit lever `TimeoutError` sur dépassement de délai et `OSError` sur échec de transport,
209
+ et **ne jamais lever** sur un statut d'erreur HTTP — sinon le code stable de l'enveloppe est perdu.
210
+
211
+ ---
212
+
213
+ ## La clé API ne s'imprime pas
214
+
215
+ ```python
216
+ journaliser(repr(senndo)) # SenndoClient(base_url='…', api_key='sk_live_…32 caractères masqués')
217
+ ```
218
+
219
+ Il n'existe aucun accesseur qui rende la clé en clair.
220
+
221
+ ---
222
+
223
+ ## Webhooks
224
+
225
+ ```python
226
+ endpoint = senndo.create_webhook(
227
+ {
228
+ "name": "Production",
229
+ "url": "https://exemple.test/senndo",
230
+ "events": ["message.sent", "message.failed"],
231
+ }
232
+ )
233
+
234
+ conserver_le_secret(endpoint["secret"])
235
+ ```
236
+
237
+ Le secret n'est lisible **qu'à la création**. Il signe chaque livraison : vérifiez la signature
238
+ avant de faire quoi que ce soit du corps.
239
+
240
+ ---
241
+
242
+ ## Licence
243
+
244
+ MIT. Voir [CHANGELOG.md](CHANGELOG.md) pour les changements de version.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "senndo"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the senndo messaging and verification API."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.11"
12
+ keywords = ["senndo", "sms", "whatsapp", "email", "voice", "otp", "messaging", "cpaas"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Communications",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ # AUCUNE DÉPENDANCE D'EXÉCUTION, ET C'EST UNE DÉCISION.
26
+ #
27
+ # `httpx` aurait donné HTTP/2, le partage de connexion et un client asynchrone gratuit. Il aurait
28
+ # aussi imposé sa version — et ses transitives — à chaque projet installant `senndo`, pour un SDK
29
+ # dont l'appel typique est un envoi unitaire depuis un worker : la connexion persistante n'y sert à
30
+ # rien, et le conflit de versions, lui, se paie chez le client. `urllib.request` couvre exactement
31
+ # ce dont le transport a besoin (verbe, en-têtes, corps, délai, code de statut), et un projet qui
32
+ # veut `httpx` ou `requests` l'injecte : le transport est un paramètre du client, pas un choix
33
+ # imposé par le paquet.
34
+ dependencies = []
35
+
36
+ [project.urls]
37
+ Homepage = "https://senndo.com"
38
+ Documentation = "https://senndo.com/developpeurs"
39
+ Changelog = "https://github.com/senndo/senndo-python/blob/main/CHANGELOG.md"
40
+ Source = "https://github.com/senndo/senndo-python"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/senndo"]
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ include = ["src/senndo", "README.md", "CHANGELOG.md", "LICENSE"]
47
+
48
+ [tool.mypy]
49
+ strict = true
50
+ python_version = "3.11"
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]