Skip to content

Commit 4707e5f

Browse files
committed
docs: fix and complete all .md files, bump to v0.7.29
1 parent 7ad2902 commit 4707e5f

7 files changed

Lines changed: 114 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
1111

1212
---
1313

14+
## [0.7.29] - 2026-03-06
15+
16+
### Fixed
17+
- `README.md`: corrected error handling example (`send()` never raises, removed incorrect `try/except RuntimeError`)
18+
- `README.md`: documented `status()`, `send_with_retry()`, `parse_webhook()`, and `AsyncKwtSMS` (were missing)
19+
- `README.md`: added `parse_webhook` to utility functions import line, added `_async.py` to repository layout
20+
- `CONTRIBUTING.md`: fixed repo URL (`kwtsms_python``kwtsms-python`), updated project structure and exports, added new test files, corrected optional dependency policy
21+
- `examples/README.md`: fixed CLI example syntax (`--phone`/`--message` flags do not exist)
22+
23+
---
24+
1425
## [0.7.28] - 2026-03-06
1526

1627
### Changed

CONTRIBUTING.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Be respectful and constructive. Treat all contributors as professionals.
1515
### 1. Clone and set up
1616

1717
```bash
18-
git clone https://github.com/boxlinknet/kwtsms_python.git
18+
git clone https://github.com/boxlinknet/kwtsms-python.git
1919
cd kwtsms_python
2020
```
2121

@@ -63,14 +63,18 @@ tests/
6363
test_api_errors.py # _enrich_error(), verify(), send() error paths
6464
test_bulk.py # _send_bulk() routing, batching, ERR013 retry
6565
test_env.py # _load_env_file(), from_env(), .env edge cases
66+
test_status.py # KwtSMS.status() delivery report lookup
67+
test_webhook.py # parse_webhook() delivery receipt parser
68+
test_async.py # AsyncKwtSMS async client
69+
test_retry.py # send_with_retry() ERR028 auto-retry
6670
test_integration.py # live API tests (skipped without credentials)
6771
```
6872

6973
---
7074

7175
## Reporting Bugs
7276

73-
Open an issue at https://github.com/boxlinknet/kwtsms_python/issues with:
77+
Open an issue at https://github.com/boxlinknet/kwtsms-python/issues with:
7478

7579
1. Python version and OS
7680
2. Package version (`pip show kwtsms`)
@@ -90,8 +94,7 @@ Open an issue before writing code for new features. Describe:
9094
2. What the API would look like (function signature, return value)
9195
3. Whether it requires a new API endpoint
9296

93-
Features that add external dependencies will not be accepted. The package
94-
is and will remain zero-dependency.
97+
Features that add mandatory external dependencies will not be accepted. The core package is zero-dependency. Optional extras (e.g., `kwtsms[async]` for aiohttp) are acceptable if declared under `[project.optional-dependencies]` in `pyproject.toml`.
9598

9699
---
97100

@@ -101,8 +104,10 @@ is and will remain zero-dependency.
101104

102105
```
103106
src/kwtsms/
104-
__init__.py # Public API: KwtSMS, clean_message, normalize_phone, validate_phone_input
105-
_core.py # All client logic
107+
__init__.py # Public API: KwtSMS, AsyncKwtSMS, clean_message, normalize_phone,
108+
# validate_phone_input, parse_webhook
109+
_core.py # All sync client logic
110+
_async.py # AsyncKwtSMS (requires kwtsms[async] / aiohttp)
106111
_cli.py # CLI entry points (kwtsms verify/send/balance/setup)
107112
108113
tests/ # pytest test suite

README.md

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ result = sms.send(["96598765432", "abc", "user@gmail.com"], "Hello")
239239
# ]
240240
```
241241

242-
**Raises** `RuntimeError` on network/HTTP failure (single send only, bulk captures errors per batch).
242+
Never raises. Network failures are returned as `{"result": "ERROR", "code": "NETWORK", "description": "...", "action": "..."}`.
243243

244244
---
245245

@@ -324,10 +324,84 @@ else:
324324

325325
---
326326

327+
### `status(msg_id)``dict`
328+
329+
Get delivery status for a sent message. Uses the `/report/` endpoint.
330+
331+
```python
332+
delivery = sms.status(result["msg-id"])
333+
if delivery["result"] == "OK":
334+
print(delivery["status"]) # "DELIVERED", "FAILED", "PENDING", "REJECTED"
335+
print(delivery["delivered-at"]) # unix timestamp (GMT+3)
336+
else:
337+
print(delivery["action"])
338+
```
339+
340+
Returns OK or ERROR dict. Never raises. Common error codes: ERR019 (no reports yet), ERR020 (ID not found), ERR021 (not ready yet).
341+
342+
---
343+
344+
### `send_with_retry(mobile, message, sender=None, max_retries=3)``dict`
345+
346+
Send SMS with automatic ERR028 retry. ERR028 means "wait 15 seconds before resending to this number." This method sleeps 16 seconds and retries automatically, up to `max_retries` times (default 3, so up to 4 total calls).
347+
348+
```python
349+
result = sms.send_with_retry("96598765432", "Your OTP is: 123456")
350+
# if ERR028 occurs: waits 16s and retries, up to 3 times
351+
```
352+
353+
Non-ERR028 errors are returned immediately without retry. Never raises.
354+
355+
---
356+
357+
### `parse_webhook(payload)``dict`
358+
359+
Parse a kwtSMS delivery receipt webhook payload. kwtSMS can POST delivery receipts to your server as JSON.
360+
361+
```python
362+
from kwtsms import parse_webhook
363+
364+
# In your webhook endpoint (Flask/FastAPI/Django):
365+
result = parse_webhook(request.json)
366+
if result["ok"]:
367+
print(result["msg_id"]) # matches the msg-id from send()
368+
print(result["phone"]) # recipient number
369+
print(result["status"]) # "DELIVERED", "FAILED", "PENDING", "REJECTED"
370+
print(result["delivered_at"]) # unix timestamp, or None
371+
else:
372+
print(result["error"]) # "Missing required field: 'msg-id'"
373+
```
374+
375+
---
376+
377+
### AsyncKwtSMS
378+
379+
Async version of `KwtSMS` for use with `asyncio`. Requires the optional `aiohttp` dependency:
380+
381+
```bash
382+
pip install kwtsms[async]
383+
```
384+
385+
```python
386+
from kwtsms import AsyncKwtSMS
387+
388+
sms = AsyncKwtSMS.from_env()
389+
390+
async def send_otp(phone: str, code: str):
391+
ok, balance, error = await sms.verify()
392+
result = await sms.send(phone, f"Your OTP is: {code}")
393+
delivery = await sms.status(result["msg-id"])
394+
return result
395+
```
396+
397+
`AsyncKwtSMS` mirrors `KwtSMS` exactly: `verify()`, `balance()`, `send()`, `status()`, `from_env()`, `purchased` property. Maximum 200 numbers per `send()` call (no auto-batching in async mode). All methods return dicts and never raise.
398+
399+
---
400+
327401
## Utility functions
328402

329403
```python
330-
from kwtsms import normalize_phone, validate_phone_input, clean_message
404+
from kwtsms import normalize_phone, validate_phone_input, clean_message, parse_webhook
331405

332406
# Normalize a phone number: strips +, 00, spaces, dashes; converts Arabic digits
333407
normalize_phone("+96598765432") # → "96598765432"
@@ -397,21 +471,16 @@ kwtsms validate 96598765432 +96512345678 0096511111111
397471

398472
## Error handling
399473

400-
Every API error response includes an `action` field with guidance:
474+
Every API error response includes an `action` field with guidance. `send()` never raises: network failures are returned as a dict with `code="NETWORK"`.
401475

402476
```python
403-
try:
404-
result = sms.send("96598765432", "Your OTP for MYAPP is: 123456")
405-
except RuntimeError as e:
406-
# Network/HTTP failure: log and retry
407-
print(f"Network error: {e}")
477+
result = sms.send("96598765432", "Your OTP for MYAPP is: 123456")
478+
if result["result"] == "OK":
479+
save_to_db(msg_id=result["msg-id"], balance=result["balance-after"])
408480
else:
409-
if result["result"] == "OK":
410-
save_to_db(msg_id=result["msg-id"], balance=result["balance-after"])
411-
else:
412-
print(result["code"]) # e.g. "ERR010"
413-
print(result["description"]) # "Account balance is zero."
414-
print(result["action"]) # "Recharge credits at kwtsms.com."
481+
print(result["code"]) # e.g. "ERR010" or "NETWORK"
482+
print(result["description"]) # "Account balance is zero."
483+
print(result["action"]) # "Recharge credits at kwtsms.com."
415484
```
416485

417486
Common error codes:
@@ -620,9 +689,12 @@ International sending is **disabled by default** on kwtSMS accounts. Contact kwt
620689
```
621690
kwtsms_python/
622691
├── src/kwtsms/
623-
│ ├── _core.py ← KwtSMS class + all logic
692+
│ ├── _core.py ← KwtSMS class + all sync logic
693+
│ ├── _async.py ← AsyncKwtSMS (requires kwtsms[async])
624694
│ ├── _cli.py ← kwtsms CLI command
625695
│ └── __init__.py ← public exports
696+
├── tests/ ← pytest test suite
697+
├── examples/ ← runnable example scripts
626698
├── pyproject.toml
627699
├── uv.lock
628700
├── README.md

examples/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ Send SMS and verify credentials from the terminal:
7878
kwtsms verify
7979

8080
# Send a message
81-
kwtsms send --phone 96598765432 --message "Hello from kwtSMS"
81+
kwtsms send 96598765432 "Hello from kwtSMS"
8282

8383
# Override sender ID for one message
84-
kwtsms send --phone 96598765432 --message "Hello" --sender MY-BRAND
84+
kwtsms send 96598765432 "Hello" --sender MY-BRAND
8585
```
8686

8787
Reads credentials from `.env` or environment variables.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "kwtsms"
3-
version = "0.7.28"
3+
version = "0.7.29"
44
description = "Python client for kwtSMS, the Kuwait SMS gateway trusted by top businesses to deliver messages worldwide, with private Sender ID, free API testing, and non-expiring credits."
55
readme = "README.md"
66
requires-python = ">=3.8"

src/kwtsms/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323

2424
__all__ = ["KwtSMS", "AsyncKwtSMS", "normalize_phone", "clean_message",
2525
"validate_phone_input", "parse_webhook"]
26-
__version__ = "0.7.28"
26+
__version__ = "0.7.29"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)