gosms-python 1.0.3__py3-none-any.whl
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.
- gosms/__init__.py +13 -0
- gosms/_base.py +69 -0
- gosms/async_client.py +167 -0
- gosms/client.py +144 -0
- gosms/django.py +80 -0
- gosms/exceptions.py +33 -0
- gosms/py.typed +0 -0
- gosms/types.py +173 -0
- gosms_python-1.0.3.dist-info/METADATA +276 -0
- gosms_python-1.0.3.dist-info/RECORD +11 -0
- gosms_python-1.0.3.dist-info/WHEEL +4 -0
gosms/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""GoSMS.GE Python SDK -- Official SMS gateway client."""
|
|
2
|
+
from gosms.client import SMS
|
|
3
|
+
from gosms.exceptions import GoSmsApiError, GoSmsErrorCode
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.3"
|
|
6
|
+
__all__ = ["SMS", "GoSmsApiError", "GoSmsErrorCode"]
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from gosms.async_client import AsyncSMS # noqa: F401
|
|
10
|
+
|
|
11
|
+
__all__.append("AsyncSMS")
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass
|
gosms/_base.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shared base logic for sync and async clients."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from gosms.exceptions import GoSmsApiError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("gosms")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _BaseClient:
|
|
13
|
+
"""Base class providing validation, URL building, and error parsing."""
|
|
14
|
+
|
|
15
|
+
BASE_URL: str = "https://api.gosms.ge/api"
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
api_key: str,
|
|
20
|
+
*,
|
|
21
|
+
timeout: int = 30,
|
|
22
|
+
retries: int = 1,
|
|
23
|
+
debug: bool = False,
|
|
24
|
+
) -> None:
|
|
25
|
+
if not api_key:
|
|
26
|
+
raise TypeError("api_key is required")
|
|
27
|
+
if not isinstance(api_key, str):
|
|
28
|
+
raise TypeError("api_key must be a string")
|
|
29
|
+
|
|
30
|
+
self._api_key = api_key
|
|
31
|
+
self._timeout = timeout
|
|
32
|
+
self._retries = max(1, retries)
|
|
33
|
+
self._debug = debug
|
|
34
|
+
|
|
35
|
+
if debug:
|
|
36
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
37
|
+
|
|
38
|
+
def _build_url(self, endpoint: str) -> str:
|
|
39
|
+
return f"{self.BASE_URL}/{endpoint}"
|
|
40
|
+
|
|
41
|
+
def _build_payload(self, **kwargs: Any) -> dict[str, Any]:
|
|
42
|
+
payload: dict[str, Any] = {"api_key": self._api_key}
|
|
43
|
+
payload.update(kwargs)
|
|
44
|
+
return payload
|
|
45
|
+
|
|
46
|
+
def _parse_response(self, data: dict[str, Any], status_code: int) -> dict[str, Any]:
|
|
47
|
+
error_code = data.get("errorCode", 0)
|
|
48
|
+
if status_code >= 400 or error_code:
|
|
49
|
+
raise GoSmsApiError(
|
|
50
|
+
error_code=error_code,
|
|
51
|
+
message=data.get("message", "Unknown error"),
|
|
52
|
+
)
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
def _validate_phone_number(self, value: Any, param_name: str) -> None:
|
|
56
|
+
if not value or not isinstance(value, str):
|
|
57
|
+
raise TypeError(f"{param_name} is required and must be a string")
|
|
58
|
+
|
|
59
|
+
def _validate_string(self, value: Any, param_name: str) -> None:
|
|
60
|
+
if not value or not isinstance(value, str):
|
|
61
|
+
raise TypeError(f"{param_name} is required and must be a string")
|
|
62
|
+
|
|
63
|
+
def _log(self, *args: Any) -> None:
|
|
64
|
+
if self._debug:
|
|
65
|
+
logger.debug("[GOSMS Debug] %s", " ".join(str(a) for a in args))
|
|
66
|
+
|
|
67
|
+
def _calculate_delay(self, attempt: int) -> float:
|
|
68
|
+
"""Exponential backoff: 1s, 2s, 4s, ..."""
|
|
69
|
+
return float(2 ** (attempt - 1))
|
gosms/async_client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Asynchronous SMS client using the httpx library."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from gosms._base import _BaseClient
|
|
10
|
+
from gosms.types import (
|
|
11
|
+
BalanceResponse,
|
|
12
|
+
CheckStatusResponse,
|
|
13
|
+
OtpSendResponse,
|
|
14
|
+
OtpVerifyResponse,
|
|
15
|
+
SendBulkSmsResponse,
|
|
16
|
+
SenderCreateResponse,
|
|
17
|
+
SmsSendResponse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncSMS(_BaseClient):
|
|
22
|
+
"""Asynchronous GoSMS.GE API client.
|
|
23
|
+
|
|
24
|
+
Can be used as an async context manager::
|
|
25
|
+
|
|
26
|
+
async with AsyncSMS('api_key') as sms:
|
|
27
|
+
result = await sms.send('995555123456', 'Hello!', 'GOSMS.GE')
|
|
28
|
+
|
|
29
|
+
Or standalone::
|
|
30
|
+
|
|
31
|
+
sms = AsyncSMS('api_key')
|
|
32
|
+
result = await sms.send('995555123456', 'Hello!', 'GOSMS.GE')
|
|
33
|
+
await sms.close()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
api_key: str,
|
|
39
|
+
*,
|
|
40
|
+
timeout: int = 30,
|
|
41
|
+
retries: int = 1,
|
|
42
|
+
debug: bool = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
super().__init__(api_key, timeout=timeout, retries=retries, debug=debug)
|
|
45
|
+
self._client: httpx.AsyncClient | None = None
|
|
46
|
+
|
|
47
|
+
async def __aenter__(self) -> AsyncSMS:
|
|
48
|
+
self._client = httpx.AsyncClient(
|
|
49
|
+
timeout=self._timeout,
|
|
50
|
+
headers={"Content-Type": "application/json"},
|
|
51
|
+
)
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
55
|
+
await self.close()
|
|
56
|
+
|
|
57
|
+
async def close(self) -> None:
|
|
58
|
+
"""Close the underlying HTTP client."""
|
|
59
|
+
if self._client is not None:
|
|
60
|
+
await self._client.aclose()
|
|
61
|
+
self._client = None
|
|
62
|
+
|
|
63
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
64
|
+
if self._client is None:
|
|
65
|
+
self._client = httpx.AsyncClient(
|
|
66
|
+
timeout=self._timeout,
|
|
67
|
+
headers={"Content-Type": "application/json"},
|
|
68
|
+
)
|
|
69
|
+
return self._client
|
|
70
|
+
|
|
71
|
+
async def _make_request(self, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
url = self._build_url(endpoint)
|
|
73
|
+
client = self._get_client()
|
|
74
|
+
last_error: Exception | None = None
|
|
75
|
+
|
|
76
|
+
for attempt in range(1, self._retries + 1):
|
|
77
|
+
try:
|
|
78
|
+
self._log(f"Request attempt {attempt}/{self._retries} to {endpoint}")
|
|
79
|
+
response = await client.post(url, json=payload)
|
|
80
|
+
data: dict[str, Any] = response.json()
|
|
81
|
+
self._log("Response:", data)
|
|
82
|
+
return self._parse_response(data, response.status_code)
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
last_error = exc
|
|
85
|
+
self._log(f"Attempt {attempt} failed:", exc)
|
|
86
|
+
if attempt < self._retries:
|
|
87
|
+
delay = self._calculate_delay(attempt)
|
|
88
|
+
self._log(f"Retrying in {delay}s...")
|
|
89
|
+
await asyncio.sleep(delay)
|
|
90
|
+
|
|
91
|
+
raise last_error # type: ignore[misc]
|
|
92
|
+
|
|
93
|
+
async def send(
|
|
94
|
+
self,
|
|
95
|
+
phone_number: str,
|
|
96
|
+
text: str,
|
|
97
|
+
sender_name: str,
|
|
98
|
+
urgent: bool = False,
|
|
99
|
+
) -> SmsSendResponse:
|
|
100
|
+
"""Send an SMS message to a single recipient."""
|
|
101
|
+
self._validate_phone_number(phone_number, "phone_number")
|
|
102
|
+
self._validate_string(text, "text")
|
|
103
|
+
self._validate_string(sender_name, "sender_name")
|
|
104
|
+
|
|
105
|
+
payload = self._build_payload(to=phone_number, text=text, urgent=urgent)
|
|
106
|
+
payload["from"] = sender_name
|
|
107
|
+
|
|
108
|
+
data = await self._make_request("sendsms", payload)
|
|
109
|
+
return SmsSendResponse.from_dict(data)
|
|
110
|
+
|
|
111
|
+
async def send_bulk(
|
|
112
|
+
self,
|
|
113
|
+
sender_name: str,
|
|
114
|
+
phone_numbers: list[str],
|
|
115
|
+
text: str,
|
|
116
|
+
urgent: bool = False,
|
|
117
|
+
no_sms_number: str | None = None,
|
|
118
|
+
) -> SendBulkSmsResponse:
|
|
119
|
+
"""Send an SMS message to multiple recipients (max 1000)."""
|
|
120
|
+
self._validate_string(sender_name, "sender_name")
|
|
121
|
+
if not isinstance(phone_numbers, list) or len(phone_numbers) == 0:
|
|
122
|
+
raise TypeError("phone_numbers is required and must be a non-empty list")
|
|
123
|
+
self._validate_string(text, "text")
|
|
124
|
+
|
|
125
|
+
payload = self._build_payload(to=phone_numbers, text=text, urgent=urgent)
|
|
126
|
+
payload["from"] = sender_name
|
|
127
|
+
if no_sms_number is not None:
|
|
128
|
+
payload["noSmsNumber"] = no_sms_number
|
|
129
|
+
|
|
130
|
+
data = await self._make_request("sendbulk", payload)
|
|
131
|
+
return SendBulkSmsResponse.from_dict(data)
|
|
132
|
+
|
|
133
|
+
async def send_otp(self, phone_number: str) -> OtpSendResponse:
|
|
134
|
+
"""Send an OTP (One-Time Password) SMS."""
|
|
135
|
+
self._validate_phone_number(phone_number, "phone_number")
|
|
136
|
+
|
|
137
|
+
data = await self._make_request("otp/send", self._build_payload(phone=phone_number))
|
|
138
|
+
return OtpSendResponse.from_dict(data)
|
|
139
|
+
|
|
140
|
+
async def verify_otp(self, phone_number: str, hash: str, code: str) -> OtpVerifyResponse:
|
|
141
|
+
"""Verify an OTP code."""
|
|
142
|
+
self._validate_phone_number(phone_number, "phone_number")
|
|
143
|
+
self._validate_string(hash, "hash")
|
|
144
|
+
self._validate_string(code, "code")
|
|
145
|
+
|
|
146
|
+
data = await self._make_request(
|
|
147
|
+
"otp/verify",
|
|
148
|
+
self._build_payload(phone=phone_number, hash=hash, code=code),
|
|
149
|
+
)
|
|
150
|
+
return OtpVerifyResponse.from_dict(data)
|
|
151
|
+
|
|
152
|
+
async def status(self, message_id: int) -> CheckStatusResponse:
|
|
153
|
+
"""Check the delivery status of a sent SMS message."""
|
|
154
|
+
data = await self._make_request("checksms", self._build_payload(messageId=message_id))
|
|
155
|
+
return CheckStatusResponse.from_dict(data)
|
|
156
|
+
|
|
157
|
+
async def balance(self) -> BalanceResponse:
|
|
158
|
+
"""Check the current SMS balance of your account."""
|
|
159
|
+
data = await self._make_request("sms-balance", self._build_payload())
|
|
160
|
+
return BalanceResponse.from_dict(data)
|
|
161
|
+
|
|
162
|
+
async def create_sender(self, name: str) -> SenderCreateResponse:
|
|
163
|
+
"""Register a new sender name."""
|
|
164
|
+
self._validate_string(name, "name")
|
|
165
|
+
|
|
166
|
+
data = await self._make_request("sender", self._build_payload(name=name))
|
|
167
|
+
return SenderCreateResponse.from_dict(data)
|
gosms/client.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Synchronous SMS client using the requests library."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from gosms._base import _BaseClient
|
|
10
|
+
from gosms.types import (
|
|
11
|
+
BalanceResponse,
|
|
12
|
+
CheckStatusResponse,
|
|
13
|
+
OtpSendResponse,
|
|
14
|
+
OtpVerifyResponse,
|
|
15
|
+
SendBulkSmsResponse,
|
|
16
|
+
SenderCreateResponse,
|
|
17
|
+
SmsSendResponse,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SMS(_BaseClient):
|
|
22
|
+
"""Synchronous GoSMS.GE API client.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
api_key: Your API key from https://gosms.ge
|
|
26
|
+
timeout: Request timeout in seconds (default: 30)
|
|
27
|
+
retries: Number of retry attempts (default: 1)
|
|
28
|
+
debug: Enable debug logging (default: False)
|
|
29
|
+
|
|
30
|
+
Example::
|
|
31
|
+
|
|
32
|
+
sms = SMS('your_api_key')
|
|
33
|
+
result = sms.send('995555123456', 'Hello!', 'GOSMS.GE')
|
|
34
|
+
print(result.message_id)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
api_key: str,
|
|
40
|
+
*,
|
|
41
|
+
timeout: int = 30,
|
|
42
|
+
retries: int = 1,
|
|
43
|
+
debug: bool = False,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(api_key, timeout=timeout, retries=retries, debug=debug)
|
|
46
|
+
self._session = requests.Session()
|
|
47
|
+
self._session.headers.update({"Content-Type": "application/json"})
|
|
48
|
+
|
|
49
|
+
def _make_request(self, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
50
|
+
url = self._build_url(endpoint)
|
|
51
|
+
last_error: Exception | None = None
|
|
52
|
+
|
|
53
|
+
for attempt in range(1, self._retries + 1):
|
|
54
|
+
try:
|
|
55
|
+
self._log(f"Request attempt {attempt}/{self._retries} to {endpoint}")
|
|
56
|
+
response = self._session.post(url, json=payload, timeout=self._timeout)
|
|
57
|
+
data: dict[str, Any] = response.json()
|
|
58
|
+
self._log("Response:", data)
|
|
59
|
+
return self._parse_response(data, response.status_code)
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
last_error = exc
|
|
62
|
+
self._log(f"Attempt {attempt} failed:", exc)
|
|
63
|
+
if attempt < self._retries:
|
|
64
|
+
delay = self._calculate_delay(attempt)
|
|
65
|
+
self._log(f"Retrying in {delay}s...")
|
|
66
|
+
time.sleep(delay)
|
|
67
|
+
|
|
68
|
+
raise last_error # type: ignore[misc]
|
|
69
|
+
|
|
70
|
+
def send(
|
|
71
|
+
self,
|
|
72
|
+
phone_number: str,
|
|
73
|
+
text: str,
|
|
74
|
+
sender_name: str,
|
|
75
|
+
urgent: bool = False,
|
|
76
|
+
) -> SmsSendResponse:
|
|
77
|
+
"""Send an SMS message to a single recipient."""
|
|
78
|
+
self._validate_phone_number(phone_number, "phone_number")
|
|
79
|
+
self._validate_string(text, "text")
|
|
80
|
+
self._validate_string(sender_name, "sender_name")
|
|
81
|
+
|
|
82
|
+
payload = self._build_payload(to=phone_number, text=text, urgent=urgent)
|
|
83
|
+
payload["from"] = sender_name
|
|
84
|
+
|
|
85
|
+
data = self._make_request("sendsms", payload)
|
|
86
|
+
return SmsSendResponse.from_dict(data)
|
|
87
|
+
|
|
88
|
+
def send_bulk(
|
|
89
|
+
self,
|
|
90
|
+
sender_name: str,
|
|
91
|
+
phone_numbers: list[str],
|
|
92
|
+
text: str,
|
|
93
|
+
urgent: bool = False,
|
|
94
|
+
no_sms_number: str | None = None,
|
|
95
|
+
) -> SendBulkSmsResponse:
|
|
96
|
+
"""Send an SMS message to multiple recipients (max 1000)."""
|
|
97
|
+
self._validate_string(sender_name, "sender_name")
|
|
98
|
+
if not isinstance(phone_numbers, list) or len(phone_numbers) == 0:
|
|
99
|
+
raise TypeError("phone_numbers is required and must be a non-empty list")
|
|
100
|
+
self._validate_string(text, "text")
|
|
101
|
+
|
|
102
|
+
payload = self._build_payload(to=phone_numbers, text=text, urgent=urgent)
|
|
103
|
+
payload["from"] = sender_name
|
|
104
|
+
if no_sms_number is not None:
|
|
105
|
+
payload["noSmsNumber"] = no_sms_number
|
|
106
|
+
|
|
107
|
+
data = self._make_request("sendbulk", payload)
|
|
108
|
+
return SendBulkSmsResponse.from_dict(data)
|
|
109
|
+
|
|
110
|
+
def send_otp(self, phone_number: str) -> OtpSendResponse:
|
|
111
|
+
"""Send an OTP (One-Time Password) SMS."""
|
|
112
|
+
self._validate_phone_number(phone_number, "phone_number")
|
|
113
|
+
|
|
114
|
+
data = self._make_request("otp/send", self._build_payload(phone=phone_number))
|
|
115
|
+
return OtpSendResponse.from_dict(data)
|
|
116
|
+
|
|
117
|
+
def verify_otp(self, phone_number: str, hash: str, code: str) -> OtpVerifyResponse:
|
|
118
|
+
"""Verify an OTP code."""
|
|
119
|
+
self._validate_phone_number(phone_number, "phone_number")
|
|
120
|
+
self._validate_string(hash, "hash")
|
|
121
|
+
self._validate_string(code, "code")
|
|
122
|
+
|
|
123
|
+
data = self._make_request(
|
|
124
|
+
"otp/verify",
|
|
125
|
+
self._build_payload(phone=phone_number, hash=hash, code=code),
|
|
126
|
+
)
|
|
127
|
+
return OtpVerifyResponse.from_dict(data)
|
|
128
|
+
|
|
129
|
+
def status(self, message_id: int) -> CheckStatusResponse:
|
|
130
|
+
"""Check the delivery status of a sent SMS message."""
|
|
131
|
+
data = self._make_request("checksms", self._build_payload(messageId=message_id))
|
|
132
|
+
return CheckStatusResponse.from_dict(data)
|
|
133
|
+
|
|
134
|
+
def balance(self) -> BalanceResponse:
|
|
135
|
+
"""Check the current SMS balance of your account."""
|
|
136
|
+
data = self._make_request("sms-balance", self._build_payload())
|
|
137
|
+
return BalanceResponse.from_dict(data)
|
|
138
|
+
|
|
139
|
+
def create_sender(self, name: str) -> SenderCreateResponse:
|
|
140
|
+
"""Register a new sender name."""
|
|
141
|
+
self._validate_string(name, "name")
|
|
142
|
+
|
|
143
|
+
data = self._make_request("sender", self._build_payload(name=name))
|
|
144
|
+
return SenderCreateResponse.from_dict(data)
|
gosms/django.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Django integration for GoSMS.GE SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from gosms.client import SMS
|
|
8
|
+
|
|
9
|
+
_client: SMS | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _ImproperlyConfigured(Exception):
|
|
13
|
+
"""Fallback when Django is not installed."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_settings() -> Any:
|
|
17
|
+
"""Import and return Django settings. Separated for testability."""
|
|
18
|
+
from django.conf import settings
|
|
19
|
+
|
|
20
|
+
return settings
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_improperly_configured() -> type:
|
|
24
|
+
"""Import and return Django's ImproperlyConfigured exception."""
|
|
25
|
+
try:
|
|
26
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
27
|
+
|
|
28
|
+
return ImproperlyConfigured
|
|
29
|
+
except ImportError:
|
|
30
|
+
return _ImproperlyConfigured
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_sms_client(**overrides: Any) -> SMS:
|
|
34
|
+
"""Get or create a singleton SMS client configured from Django settings.
|
|
35
|
+
|
|
36
|
+
Reads from ``settings.GOSMS_SETTINGS``::
|
|
37
|
+
|
|
38
|
+
# settings.py
|
|
39
|
+
GOSMS_SETTINGS = {
|
|
40
|
+
'api_key': 'your_key',
|
|
41
|
+
'timeout': 30, # optional
|
|
42
|
+
'retries': 1, # optional
|
|
43
|
+
'debug': False, # optional (defaults to DEBUG)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Usage::
|
|
47
|
+
|
|
48
|
+
from gosms.django import get_sms_client
|
|
49
|
+
sms = get_sms_client()
|
|
50
|
+
sms.send('995555123456', 'Hello!', 'GOSMS.GE')
|
|
51
|
+
"""
|
|
52
|
+
global _client
|
|
53
|
+
if _client is None:
|
|
54
|
+
from gosms.client import SMS
|
|
55
|
+
|
|
56
|
+
settings = _get_settings()
|
|
57
|
+
ImproperlyConfigured = _get_improperly_configured()
|
|
58
|
+
|
|
59
|
+
config: dict[str, Any] = getattr(settings, "GOSMS_SETTINGS", {})
|
|
60
|
+
api_key = config.get("api_key")
|
|
61
|
+
if not api_key:
|
|
62
|
+
raise ImproperlyConfigured(
|
|
63
|
+
"GOSMS_SETTINGS must contain 'api_key'. "
|
|
64
|
+
"Add GOSMS_SETTINGS = {'api_key': '...'} to your Django settings."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
opts: dict[str, Any] = {
|
|
68
|
+
"timeout": config.get("timeout", 30),
|
|
69
|
+
"retries": config.get("retries", 1),
|
|
70
|
+
"debug": config.get("debug", getattr(settings, "DEBUG", False)),
|
|
71
|
+
}
|
|
72
|
+
opts.update(overrides)
|
|
73
|
+
_client = SMS(api_key, **opts)
|
|
74
|
+
return _client
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def reset_client() -> None:
|
|
78
|
+
"""Reset the singleton client. Useful for testing."""
|
|
79
|
+
global _client
|
|
80
|
+
_client = None
|
gosms/exceptions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Exception types and error codes for the GoSMS.GE SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GoSmsErrorCode:
|
|
6
|
+
"""Error code constants matching the GoSMS.GE API."""
|
|
7
|
+
|
|
8
|
+
INVALID_API_KEY = 100
|
|
9
|
+
INVALID_SENDER = 101
|
|
10
|
+
INSUFFICIENT_BALANCE = 102
|
|
11
|
+
INVALID_PARAMETERS = 103
|
|
12
|
+
MESSAGE_NOT_FOUND = 104
|
|
13
|
+
INVALID_PHONE = 105
|
|
14
|
+
OTP_FAILED = 106
|
|
15
|
+
SENDER_EXISTS = 107
|
|
16
|
+
NOT_CONFIGURED = 108
|
|
17
|
+
TOO_MANY_REQUESTS = 109
|
|
18
|
+
ACCOUNT_LOCKED = 110
|
|
19
|
+
OTP_EXPIRED = 111
|
|
20
|
+
OTP_ALREADY_USED = 112
|
|
21
|
+
INVALID_NO_SMS_NUMBER = 113
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GoSmsApiError(Exception):
|
|
25
|
+
"""Raised when the GoSMS.GE API returns an error response."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, error_code: int, message: str) -> None:
|
|
28
|
+
self.error_code = error_code
|
|
29
|
+
self.message = message
|
|
30
|
+
super().__init__(f"[{error_code}] {message}")
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"GoSmsApiError(error_code={self.error_code}, message={self.message!r})"
|
gosms/py.typed
ADDED
|
File without changes
|
gosms/types.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Response type definitions matching the GoSMS.GE API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class SmsSendResponse:
|
|
10
|
+
success: bool
|
|
11
|
+
message_id: int
|
|
12
|
+
sender: str
|
|
13
|
+
to: str
|
|
14
|
+
text: str
|
|
15
|
+
send_at: str
|
|
16
|
+
balance: int
|
|
17
|
+
encode: str
|
|
18
|
+
segment: int
|
|
19
|
+
sms_characters: int
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_dict(cls, data: dict[str, Any]) -> SmsSendResponse:
|
|
23
|
+
return cls(
|
|
24
|
+
success=data.get("success", False),
|
|
25
|
+
message_id=data.get("messageId", 0),
|
|
26
|
+
sender=data.get("from", ""),
|
|
27
|
+
to=data.get("to", ""),
|
|
28
|
+
text=data.get("text", ""),
|
|
29
|
+
send_at=data.get("sendAt", ""),
|
|
30
|
+
balance=data.get("balance", 0),
|
|
31
|
+
encode=data.get("encode", ""),
|
|
32
|
+
segment=data.get("segment", 0),
|
|
33
|
+
sms_characters=data.get("smsCharacters", 0),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class BulkSmsResult:
|
|
39
|
+
message_id: int
|
|
40
|
+
to: str
|
|
41
|
+
success: bool
|
|
42
|
+
error: str | None = None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_dict(cls, data: dict[str, Any]) -> BulkSmsResult:
|
|
46
|
+
return cls(
|
|
47
|
+
message_id=data.get("messageId", 0),
|
|
48
|
+
to=data.get("to", ""),
|
|
49
|
+
success=data.get("success", False),
|
|
50
|
+
error=data.get("error"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SendBulkSmsResponse:
|
|
56
|
+
success: bool
|
|
57
|
+
total_count: int
|
|
58
|
+
success_count: int
|
|
59
|
+
failed_count: int
|
|
60
|
+
balance: int
|
|
61
|
+
sender: str
|
|
62
|
+
text: str
|
|
63
|
+
encode: str
|
|
64
|
+
segment: int
|
|
65
|
+
sms_characters: int
|
|
66
|
+
messages: list[BulkSmsResult] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: dict[str, Any]) -> SendBulkSmsResponse:
|
|
70
|
+
return cls(
|
|
71
|
+
success=data.get("success", False),
|
|
72
|
+
total_count=data.get("totalCount", 0),
|
|
73
|
+
success_count=data.get("successCount", 0),
|
|
74
|
+
failed_count=data.get("failedCount", 0),
|
|
75
|
+
balance=data.get("balance", 0),
|
|
76
|
+
sender=data.get("from", ""),
|
|
77
|
+
text=data.get("text", ""),
|
|
78
|
+
encode=data.get("encode", ""),
|
|
79
|
+
segment=data.get("segment", 0),
|
|
80
|
+
sms_characters=data.get("smsCharacters", 0),
|
|
81
|
+
messages=[BulkSmsResult.from_dict(m) for m in data.get("messages", [])],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class CheckStatusResponse:
|
|
87
|
+
success: bool
|
|
88
|
+
message_id: int
|
|
89
|
+
sender: str
|
|
90
|
+
to: str
|
|
91
|
+
text: str
|
|
92
|
+
send_at: str
|
|
93
|
+
encode: str
|
|
94
|
+
segment: int
|
|
95
|
+
sms_characters: int
|
|
96
|
+
status: str
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, data: dict[str, Any]) -> CheckStatusResponse:
|
|
100
|
+
return cls(
|
|
101
|
+
success=data.get("success", False),
|
|
102
|
+
message_id=data.get("messageId", 0),
|
|
103
|
+
sender=data.get("from", ""),
|
|
104
|
+
to=data.get("to", ""),
|
|
105
|
+
text=data.get("text", ""),
|
|
106
|
+
send_at=data.get("sendAt", ""),
|
|
107
|
+
encode=data.get("encode", ""),
|
|
108
|
+
segment=data.get("segment", 0),
|
|
109
|
+
sms_characters=data.get("smsCharacters", 0),
|
|
110
|
+
status=data.get("status", ""),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class BalanceResponse:
|
|
116
|
+
success: bool
|
|
117
|
+
balance: int
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, data: dict[str, Any]) -> BalanceResponse:
|
|
121
|
+
return cls(
|
|
122
|
+
success=data.get("success", False),
|
|
123
|
+
balance=data.get("balance", 0),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class OtpSendResponse:
|
|
129
|
+
success: bool
|
|
130
|
+
hash: str
|
|
131
|
+
balance: int
|
|
132
|
+
to: str
|
|
133
|
+
send_at: str
|
|
134
|
+
encode: str
|
|
135
|
+
segment: int
|
|
136
|
+
sms_characters: int
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_dict(cls, data: dict[str, Any]) -> OtpSendResponse:
|
|
140
|
+
return cls(
|
|
141
|
+
success=data.get("success", False),
|
|
142
|
+
hash=data.get("hash", ""),
|
|
143
|
+
balance=data.get("balance", 0),
|
|
144
|
+
to=data.get("to", ""),
|
|
145
|
+
send_at=data.get("sendAt", ""),
|
|
146
|
+
encode=data.get("encode", ""),
|
|
147
|
+
segment=data.get("segment", 0),
|
|
148
|
+
sms_characters=data.get("smsCharacters", 0),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class OtpVerifyResponse:
|
|
154
|
+
success: bool
|
|
155
|
+
verify: bool
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_dict(cls, data: dict[str, Any]) -> OtpVerifyResponse:
|
|
159
|
+
return cls(
|
|
160
|
+
success=data.get("success", False),
|
|
161
|
+
verify=data.get("verify", False),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class SenderCreateResponse:
|
|
167
|
+
success: bool
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def from_dict(cls, data: dict[str, Any]) -> SenderCreateResponse:
|
|
171
|
+
return cls(
|
|
172
|
+
success=data.get("success", False),
|
|
173
|
+
)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gosms-python
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: Official Python SDK for GoSMS.GE SMS Gateway
|
|
5
|
+
Project-URL: Homepage, https://gosms.ge
|
|
6
|
+
Project-URL: Repository, https://github.com/gosms-ge/gosmsge-python
|
|
7
|
+
Project-URL: Issues, https://github.com/gosms-ge/gosmsge-python/issues
|
|
8
|
+
Author-email: "GoSMS.GE" <info@gosms.ge>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: gosms,otp,sms,sms-api,sms-gateway,sms-sdk
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Communications
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Provides-Extra: async
|
|
25
|
+
Requires-Dist: httpx>=0.24.0; extra == 'async'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-httpx>=0.21; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: responses>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
33
|
+
Provides-Extra: django
|
|
34
|
+
Requires-Dist: django>=3.2; extra == 'django'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# GoSMS.GE Python SDK
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/gosms-python/)
|
|
40
|
+
[](https://pypi.org/project/gosms-python/)
|
|
41
|
+
[](https://github.com/gosms-python/gosms-python-python/actions)
|
|
42
|
+
[](https://opensource.org/licenses/MIT)
|
|
43
|
+
|
|
44
|
+
Official Python SDK for the [GoSMS.GE](https://gosms.ge) SMS gateway. Send SMS messages, manage OTP verification, and check balances with both sync and async clients.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install gosms-python
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For async support:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install gosms-python[async]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from gosms import SMS
|
|
62
|
+
|
|
63
|
+
sms = SMS("your_api_key")
|
|
64
|
+
|
|
65
|
+
# Send a message
|
|
66
|
+
result = sms.send("995555123456", "Hello!", "GOSMS.GE")
|
|
67
|
+
print(result.message_id) # 12345
|
|
68
|
+
print(result.balance) # 99
|
|
69
|
+
|
|
70
|
+
# Check balance
|
|
71
|
+
balance = sms.balance()
|
|
72
|
+
print(balance.balance) # 500
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## All Endpoints
|
|
76
|
+
|
|
77
|
+
### Send SMS
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
result = sms.send("995555123456", "Hello!", "GOSMS.GE")
|
|
81
|
+
result = sms.send("995555123456", "Urgent!", "GOSMS.GE", urgent=True)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Send Bulk SMS
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
result = sms.send_bulk(
|
|
88
|
+
"GOSMS.GE",
|
|
89
|
+
["995555111111", "995555222222"],
|
|
90
|
+
"Hello everyone!",
|
|
91
|
+
)
|
|
92
|
+
print(result.total_count) # 2
|
|
93
|
+
print(result.success_count) # 2
|
|
94
|
+
|
|
95
|
+
for msg in result.messages:
|
|
96
|
+
print(f"{msg.to}: {msg.message_id}")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Send OTP
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
result = sms.send_otp("995555123456")
|
|
103
|
+
print(result.hash) # "abc123hash" — save this for verification
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Verify OTP
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
result = sms.verify_otp("995555123456", "abc123hash", "1234")
|
|
110
|
+
print(result.verify) # True
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Check Message Status
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
result = sms.status(12345)
|
|
117
|
+
print(result.status) # "delivered"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Check Balance
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
result = sms.balance()
|
|
124
|
+
print(result.balance) # 500
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Create Sender Name
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
result = sms.create_sender("MyBrand")
|
|
131
|
+
print(result.success) # True
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Async Usage
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import asyncio
|
|
138
|
+
from gosms import AsyncSMS
|
|
139
|
+
|
|
140
|
+
async def main():
|
|
141
|
+
async with AsyncSMS("your_api_key") as sms:
|
|
142
|
+
result = await sms.send("995555123456", "Hello!", "GOSMS.GE")
|
|
143
|
+
print(result.message_id)
|
|
144
|
+
|
|
145
|
+
balance = await sms.balance()
|
|
146
|
+
print(balance.balance)
|
|
147
|
+
|
|
148
|
+
asyncio.run(main())
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
All methods from the sync client are available as async equivalents with the same signatures.
|
|
152
|
+
|
|
153
|
+
## Django Integration
|
|
154
|
+
|
|
155
|
+
Add to your `settings.py`:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
GOSMS_SETTINGS = {
|
|
159
|
+
"api_key": "your_api_key",
|
|
160
|
+
"timeout": 30, # optional
|
|
161
|
+
"retries": 1, # optional
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Use anywhere in your project:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from gosms.django import get_sms_client
|
|
169
|
+
|
|
170
|
+
sms = get_sms_client()
|
|
171
|
+
sms.send("995555123456", "Hello!", "GOSMS.GE")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The client is created lazily on first call and reused as a singleton.
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
sms = SMS(
|
|
180
|
+
"your_api_key",
|
|
181
|
+
timeout=30, # request timeout in seconds (default: 30)
|
|
182
|
+
retries=3, # retry attempts on failure (default: 1)
|
|
183
|
+
debug=True, # enable debug logging (default: False)
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Error Handling
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from gosms import SMS, GoSmsApiError, GoSmsErrorCode
|
|
191
|
+
|
|
192
|
+
sms = SMS("your_api_key")
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
result = sms.send("995555123456", "Hello!", "GOSMS.GE")
|
|
196
|
+
except GoSmsApiError as e:
|
|
197
|
+
print(e.error_code) # 100
|
|
198
|
+
print(e.message) # "Invalid API key"
|
|
199
|
+
|
|
200
|
+
if e.error_code == GoSmsErrorCode.INVALID_API_KEY:
|
|
201
|
+
print("Check your API key")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Error Codes
|
|
205
|
+
|
|
206
|
+
| Code | Constant | Description |
|
|
207
|
+
|------|----------|-------------|
|
|
208
|
+
| 100 | `INVALID_API_KEY` | Invalid API key |
|
|
209
|
+
| 101 | `INVALID_PHONE_NUMBER` | Invalid phone number |
|
|
210
|
+
| 102 | `INSUFFICIENT_BALANCE` | Insufficient balance |
|
|
211
|
+
| 103 | `SENDER_NOT_FOUND` | Sender name not found |
|
|
212
|
+
| 104 | `INVALID_TEXT` | Invalid message text |
|
|
213
|
+
| 105 | `TOO_MANY_RECIPIENTS` | Too many recipients (max 1000) |
|
|
214
|
+
| 106 | `INVALID_MESSAGE_ID` | Invalid message ID |
|
|
215
|
+
| 107 | `SENDER_EXISTS` | Sender name already exists |
|
|
216
|
+
| 108 | `INVALID_SENDER_NAME` | Invalid sender name |
|
|
217
|
+
| 109 | `INVALID_OTP_HASH` | Invalid OTP hash |
|
|
218
|
+
| 110 | `INVALID_OTP_CODE` | Invalid OTP code |
|
|
219
|
+
| 111 | `OTP_EXPIRED` | OTP expired |
|
|
220
|
+
| 112 | `OTP_ALREADY_VERIFIED` | OTP already verified |
|
|
221
|
+
| 113 | `RATE_LIMIT_EXCEEDED` | Rate limit exceeded |
|
|
222
|
+
|
|
223
|
+
## Response Types
|
|
224
|
+
|
|
225
|
+
All methods return typed frozen dataclasses:
|
|
226
|
+
|
|
227
|
+
| Method | Return Type | Key Fields |
|
|
228
|
+
|--------|-------------|------------|
|
|
229
|
+
| `send()` | `SmsSendResponse` | `success`, `message_id`, `balance` |
|
|
230
|
+
| `send_bulk()` | `SendBulkSmsResponse` | `success`, `total_count`, `messages` |
|
|
231
|
+
| `send_otp()` | `OtpSendResponse` | `success`, `hash`, `balance` |
|
|
232
|
+
| `verify_otp()` | `OtpVerifyResponse` | `success`, `verify` |
|
|
233
|
+
| `status()` | `CheckStatusResponse` | `success`, `status`, `message_id` |
|
|
234
|
+
| `balance()` | `BalanceResponse` | `success`, `balance` |
|
|
235
|
+
| `create_sender()` | `SenderCreateResponse` | `success` |
|
|
236
|
+
|
|
237
|
+
## Migration from v1.x
|
|
238
|
+
|
|
239
|
+
v2.0 is a complete rewrite. Key changes:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# v1.x (old)
|
|
243
|
+
from gosms import sms # module-level singleton
|
|
244
|
+
sms.send('995...', 'text', 'SENDER')
|
|
245
|
+
|
|
246
|
+
# v2.0 (new)
|
|
247
|
+
from gosms import SMS # explicit instantiation
|
|
248
|
+
sms = SMS('your_api_key')
|
|
249
|
+
sms.send('995...', 'text', 'SENDER')
|
|
250
|
+
|
|
251
|
+
# v1.x Django (old)
|
|
252
|
+
from gosms import sms # import-time side effect
|
|
253
|
+
|
|
254
|
+
# v2.0 Django (new)
|
|
255
|
+
from gosms.django import get_sms_client # lazy factory
|
|
256
|
+
sms = get_sms_client()
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Other changes:
|
|
260
|
+
- `GoSmsApiError` now extends `Exception` (was `BaseException`)
|
|
261
|
+
- Added `GoSmsErrorCode` constants for typed error handling
|
|
262
|
+
- Added `send_bulk()` and `create_sender()` endpoints
|
|
263
|
+
- Added async client (`AsyncSMS`) via `pip install gosms-python[async]`
|
|
264
|
+
- All responses are typed frozen dataclasses
|
|
265
|
+
- Removed `dev_mode` / `RequestMock` in favor of standard test mocking
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|
|
270
|
+
|
|
271
|
+
## Links
|
|
272
|
+
|
|
273
|
+
- Website: https://gosms.ge
|
|
274
|
+
- PyPI: https://pypi.org/project/gosms-python/
|
|
275
|
+
- GitHub: https://github.com/gosms-python/gosms-python-python
|
|
276
|
+
- Support: info@gosms.ge
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
gosms/__init__.py,sha256=MtJSvCMP5rqx_Yi-Fzw9bJ7cXtwHV9In_4OJXcHs5z0,347
|
|
2
|
+
gosms/_base.py,sha256=mRtlZ-iMxGouswbVwNZvJulbzLB5AYF0_34Lh7A7nMQ,2210
|
|
3
|
+
gosms/async_client.py,sha256=OSZEyGFcSAiEvLttQNM4WhzF8GwSqB2GM3iS6d5Hixs,5924
|
|
4
|
+
gosms/client.py,sha256=5u0F-wUpy2eItn8X7IYF-h7WHMoEV8omfTbvSqTpGTI,5175
|
|
5
|
+
gosms/django.py,sha256=R8__y8LbWTcv2zgtOVwRcUnzjqT5B_faj3u3yZpCwLs,2246
|
|
6
|
+
gosms/exceptions.py,sha256=fpUslXvA3oNFb4eixbJ5s5tJgM4ew_-7FymKBz0dYGs,951
|
|
7
|
+
gosms/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
gosms/types.py,sha256=9Lu13z3DRUqODb_gu0nFivRd7cX0JKCq6ZFk4oVP1r4,4580
|
|
9
|
+
gosms_python-1.0.3.dist-info/METADATA,sha256=odYFHrYQgRgmeiRR-u7s3LwMlkUU8Qkhkhd4sOJxyF8,7533
|
|
10
|
+
gosms_python-1.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
gosms_python-1.0.3.dist-info/RECORD,,
|