@@ -16,12 +16,14 @@ async def main():
1616
1717import json
1818import os
19+ from datetime import datetime , timezone
1920from typing import List , Optional , Union
2021
2122from kwtsms ._core import (
2223 BASE_URL ,
2324 _enrich_error ,
2425 _load_env_file ,
26+ _write_log ,
2527 clean_message ,
2628 validate_phone_input ,
2729)
@@ -40,14 +42,34 @@ async def _async_request(endpoint: str, payload: dict, log_file: str = "") -> di
4042 "aiohttp is required for async usage. Install with: pip install kwtsms[async]"
4143 )
4244 url = BASE_URL + endpoint + "/"
43- async with aiohttp .ClientSession () as session :
44- async with session .post (url , json = payload ,
45- timeout = aiohttp .ClientTimeout (total = 15 )) as resp :
46- text = await resp .text ()
47- try :
48- return json .loads (text )
49- except json .JSONDecodeError as exc :
50- raise RuntimeError (f"Invalid JSON response: { exc } " ) from exc
45+ safe_payload = {k : ("***" if k == "password" else v ) for k , v in payload .items ()}
46+ log_entry = {
47+ "ts" : datetime .now (timezone .utc ).isoformat (),
48+ "endpoint" : endpoint ,
49+ "request" : safe_payload ,
50+ "response" : None ,
51+ "ok" : False ,
52+ "error" : None ,
53+ }
54+ try :
55+ async with aiohttp .ClientSession () as session :
56+ async with session .post (url , json = payload ,
57+ timeout = aiohttp .ClientTimeout (total = 15 )) as resp :
58+ text = await resp .text ()
59+ try :
60+ data = json .loads (text )
61+ except json .JSONDecodeError as exc :
62+ log_entry ["error" ] = f"Invalid JSON response: { exc } "
63+ _write_log (log_file , log_entry )
64+ raise RuntimeError (f"Invalid JSON response: { exc } " ) from exc
65+ log_entry ["response" ] = data
66+ log_entry ["ok" ] = data .get ("result" ) == "OK"
67+ _write_log (log_file , log_entry )
68+ return data
69+ except aiohttp .ClientError as exc :
70+ log_entry ["error" ] = f"Network error: { exc } "
71+ _write_log (log_file , log_entry )
72+ raise RuntimeError (f"Network error: { exc } " ) from exc
5173
5274
5375class AsyncKwtSMS :
@@ -64,6 +86,16 @@ class AsyncKwtSMS:
6486
6587 def __init__ (self , username : str , password : str , sender_id : str = "KWT-SMS" ,
6688 test_mode : bool = False , log_file : str = "kwtsms.log" ):
89+ """
90+ Create an AsyncKwtSMS client.
91+
92+ Args:
93+ username: kwtSMS API username
94+ password: kwtSMS API password
95+ sender_id: default Sender ID shown on recipient's phone
96+ test_mode: if True, sends are billed as test (no credits consumed)
97+ log_file: path to JSONL request log file, or "" to disable logging
98+ """
6799 if not username or not password :
68100 raise ValueError ("username and password are required" )
69101 self .username = username
@@ -107,6 +139,7 @@ def _creds(self) -> dict:
107139
108140 @property
109141 def purchased (self ) -> Optional [float ]:
142+ """Total credits purchased. None before the first successful verify() call."""
110143 return self ._cached_purchased
111144
112145 async def verify (self ) -> tuple :
@@ -125,7 +158,7 @@ async def verify(self) -> tuple:
125158 return False , None , str (exc )
126159
127160 async def balance (self ) -> Optional [float ]:
128- """Get current balance. Returns None on error."""
161+ """Get current balance. Returns None on error (or last cached value if available) ."""
129162 ok , bal , _ = await self .verify ()
130163 return bal if ok else self ._cached_balance
131164
@@ -134,6 +167,8 @@ async def send(self, mobile: Union[str, List[str]], message: str,
134167 """
135168 Send SMS to one or more numbers. Same contract as KwtSMS.send().
136169
170+ Note: Maximum 200 numbers per call. For larger lists, split into batches.
171+
137172 Returns OK or ERROR dict; never raises.
138173 """
139174 effective_sender = sender or self .sender_id
@@ -154,6 +189,13 @@ async def send(self, mobile: Union[str, List[str]], message: str,
154189 return _enrich_error ({"result" : "ERROR" , "code" : "ERR_INVALID_INPUT" ,
155190 "description" : description , "invalid" : invalid })
156191
192+ if len (valid_numbers ) > 200 :
193+ return _enrich_error ({
194+ "result" : "ERROR" ,
195+ "code" : "ERR007" ,
196+ "description" : f"Too many numbers ({ len (valid_numbers )} ). Maximum 200 per call. For larger lists, call send() in batches." ,
197+ })
198+
157199 cleaned = clean_message (message )
158200 if not cleaned :
159201 return _enrich_error ({"result" : "ERROR" , "code" : "ERR009" ,
0 commit comments