sendgo-python 1.0.1__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.
- sendgo/__init__.py +25 -0
- sendgo/alimtalk.py +57 -0
- sendgo/client.py +50 -0
- sendgo/exceptions.py +52 -0
- sendgo/friendtalk.py +66 -0
- sendgo/http_client.py +48 -0
- sendgo/sms.py +57 -0
- sendgo/token_manager.py +70 -0
- sendgo_python-1.0.1.dist-info/METADATA +428 -0
- sendgo_python-1.0.1.dist-info/RECORD +12 -0
- sendgo_python-1.0.1.dist-info/WHEEL +5 -0
- sendgo_python-1.0.1.dist-info/top_level.txt +1 -0
sendgo/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sendgo Python SDK — 카카오 알림톡/친구톡, SMS/LMS/MMS
|
|
3
|
+
|
|
4
|
+
사용법:
|
|
5
|
+
from sendgo import Sendgo
|
|
6
|
+
|
|
7
|
+
client = Sendgo(
|
|
8
|
+
access_key="your_access_key",
|
|
9
|
+
secret_key="your_secret_key",
|
|
10
|
+
kakao_sender_key="your_kakao_key",
|
|
11
|
+
sms_sender_key="your_sms_key",
|
|
12
|
+
api_version="v2",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
client.alimtalk.send(
|
|
16
|
+
template_code="ORDER_CONFIRM_001",
|
|
17
|
+
contacts=[{"contact": "01012345678", "var1": "ORD-001"}],
|
|
18
|
+
)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .client import Sendgo
|
|
22
|
+
from .exceptions import SendgoError
|
|
23
|
+
|
|
24
|
+
__all__ = ["Sendgo", "SendgoError"]
|
|
25
|
+
__version__ = "1.0.0"
|
sendgo/alimtalk.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from .http_client import HttpClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AlimtalkService:
|
|
9
|
+
"""카카오 알림톡 전송 서비스.
|
|
10
|
+
|
|
11
|
+
Example::
|
|
12
|
+
|
|
13
|
+
client.alimtalk.send(
|
|
14
|
+
template_code="ORDER_CONFIRM_001",
|
|
15
|
+
contacts=[{"contact": "01012345678", "var1": "ORD-001"}],
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, http: HttpClient, kakao_sender_key: str | None, sms_sender_key: str | None) -> None:
|
|
20
|
+
self._http = http
|
|
21
|
+
self._kakao_sender_key = kakao_sender_key
|
|
22
|
+
self._sms_sender_key = sms_sender_key
|
|
23
|
+
|
|
24
|
+
def send(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
template_code: str,
|
|
28
|
+
contacts: list[dict],
|
|
29
|
+
schedule_type: Literal["DIRECTLY", "SCHEDULED"] = "DIRECTLY",
|
|
30
|
+
at: str | None = None,
|
|
31
|
+
replace_sms: Literal["Y", "N"] = "N",
|
|
32
|
+
sms_subject: str | None = None,
|
|
33
|
+
sms_content: str | None = None,
|
|
34
|
+
) -> dict:
|
|
35
|
+
"""알림톡을 전송합니다.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
template_code: 승인된 알림톡 템플릿 코드 (필수)
|
|
39
|
+
contacts: 수신자 목록. 각 dict에 contact(필수), name, var1~var8 포함 가능
|
|
40
|
+
schedule_type: 발송 유형 (DIRECTLY | SCHEDULED)
|
|
41
|
+
at: 예약 발송 시각 (예: "2026-04-01 09:00:00")
|
|
42
|
+
replace_sms: 알림톡 실패 시 SMS 대체 발송 여부
|
|
43
|
+
sms_subject: 대체 SMS 제목
|
|
44
|
+
sms_content: 대체 SMS 내용
|
|
45
|
+
"""
|
|
46
|
+
body: dict = {
|
|
47
|
+
"at": at,
|
|
48
|
+
"scheduleType": schedule_type,
|
|
49
|
+
"templateCode": template_code,
|
|
50
|
+
"replaceSms": replace_sms,
|
|
51
|
+
"smsSubject": sms_subject if replace_sms == "Y" else None,
|
|
52
|
+
"smsContent": sms_content if replace_sms == "Y" else None,
|
|
53
|
+
"contacts": contacts,
|
|
54
|
+
"kakaoSenderKey": self._kakao_sender_key,
|
|
55
|
+
"senderKey": self._sms_sender_key,
|
|
56
|
+
}
|
|
57
|
+
return self._http.post("notices/send", body)
|
sendgo/client.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .alimtalk import AlimtalkService
|
|
4
|
+
from .friendtalk import FriendtalkService
|
|
5
|
+
from .http_client import HttpClient
|
|
6
|
+
from .sms import SmsService
|
|
7
|
+
from .token_manager import TokenManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Sendgo:
|
|
11
|
+
"""Sendgo Python SDK 메인 클라이언트.
|
|
12
|
+
|
|
13
|
+
Example::
|
|
14
|
+
|
|
15
|
+
from sendgo import Sendgo
|
|
16
|
+
|
|
17
|
+
client = Sendgo(
|
|
18
|
+
access_key="your_access_key",
|
|
19
|
+
secret_key="your_secret_key",
|
|
20
|
+
kakao_sender_key="your_kakao_key",
|
|
21
|
+
sms_sender_key="your_sms_key",
|
|
22
|
+
api_version="v2",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# 알림톡 전송
|
|
26
|
+
client.alimtalk.send(
|
|
27
|
+
template_code="ORDER_CONFIRM_001",
|
|
28
|
+
contacts=[{"contact": "01012345678", "var1": "ORD-001"}],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# SMS 전송
|
|
32
|
+
client.sms.send_sms(content="인증번호: 123456", contacts=[{"contact": "01012345678"}])
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
access_key: str,
|
|
39
|
+
secret_key: str,
|
|
40
|
+
kakao_sender_key: str | None = None,
|
|
41
|
+
sms_sender_key: str | None = None,
|
|
42
|
+
api_version: str = "v1",
|
|
43
|
+
base_url: str = "https://sendgo.io",
|
|
44
|
+
) -> None:
|
|
45
|
+
token_manager = TokenManager(base_url, access_key, secret_key, api_version)
|
|
46
|
+
http = HttpClient(token_manager, base_url, api_version)
|
|
47
|
+
|
|
48
|
+
self.alimtalk = AlimtalkService(http, kakao_sender_key, sms_sender_key)
|
|
49
|
+
self.friendtalk = FriendtalkService(http, kakao_sender_key, sms_sender_key)
|
|
50
|
+
self.sms = SmsService(http, sms_sender_key)
|
sendgo/exceptions.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SendgoError(Exception):
|
|
5
|
+
"""Sendgo API 호출 실패 시 발생하는 예외."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
*,
|
|
11
|
+
status_code: int = 0,
|
|
12
|
+
error_code: str | None = None,
|
|
13
|
+
endpoint: str = "",
|
|
14
|
+
api_version: str = "",
|
|
15
|
+
response_body: dict | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.error_code = error_code
|
|
20
|
+
self.endpoint = endpoint
|
|
21
|
+
self.api_version = api_version
|
|
22
|
+
self.response_body = response_body or {}
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_response(
|
|
26
|
+
cls,
|
|
27
|
+
status: int,
|
|
28
|
+
body: dict,
|
|
29
|
+
endpoint: str,
|
|
30
|
+
api_version: str,
|
|
31
|
+
) -> "SendgoError":
|
|
32
|
+
error_code = body.get("code")
|
|
33
|
+
error_message = body.get("message", "Unknown error")
|
|
34
|
+
msg = f"HTTP {status}"
|
|
35
|
+
if error_code:
|
|
36
|
+
msg += f" [{error_code}]"
|
|
37
|
+
msg += f" {error_message}"
|
|
38
|
+
return cls(
|
|
39
|
+
msg,
|
|
40
|
+
status_code=status,
|
|
41
|
+
error_code=error_code,
|
|
42
|
+
endpoint=endpoint,
|
|
43
|
+
api_version=api_version,
|
|
44
|
+
response_body=body,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return (
|
|
49
|
+
f"SendgoError(status={self.status_code}, "
|
|
50
|
+
f"error_code={self.error_code!r}, "
|
|
51
|
+
f"endpoint={self.endpoint!r})"
|
|
52
|
+
)
|
sendgo/friendtalk.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from .http_client import HttpClient
|
|
6
|
+
|
|
7
|
+
FriendtalkMessageType = Literal["FT", "FI", "FW", "FL", "FM", "FC", "FA", "FP"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FriendtalkService:
|
|
11
|
+
"""카카오 친구톡 전송 서비스.
|
|
12
|
+
|
|
13
|
+
Example::
|
|
14
|
+
|
|
15
|
+
client.friendtalk.send(
|
|
16
|
+
content="안녕하세요! 이번 주 특가 이벤트입니다.",
|
|
17
|
+
contacts=[{"contact": "01012345678"}],
|
|
18
|
+
)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, http: HttpClient, kakao_sender_key: str | None, sms_sender_key: str | None) -> None:
|
|
22
|
+
self._http = http
|
|
23
|
+
self._kakao_sender_key = kakao_sender_key
|
|
24
|
+
self._sms_sender_key = sms_sender_key
|
|
25
|
+
|
|
26
|
+
def send(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
content: str,
|
|
30
|
+
contacts: list[dict],
|
|
31
|
+
message_type: FriendtalkMessageType = "FT",
|
|
32
|
+
schedule_type: Literal["DIRECTLY", "SCHEDULED"] = "DIRECTLY",
|
|
33
|
+
at: str | None = None,
|
|
34
|
+
buttons: list[dict] | None = None,
|
|
35
|
+
image_url: str | None = None,
|
|
36
|
+
image_link: str | None = None,
|
|
37
|
+
ad_flag: Literal["Y", "N"] = "Y",
|
|
38
|
+
wide: Literal["Y", "N"] = "N",
|
|
39
|
+
adult: Literal["Y", "N"] = "N",
|
|
40
|
+
header: str | None = None,
|
|
41
|
+
replace_sms: Literal["Y", "N"] = "N",
|
|
42
|
+
sms_subject: str | None = None,
|
|
43
|
+
sms_content: str | None = None,
|
|
44
|
+
) -> dict:
|
|
45
|
+
"""친구톡을 전송합니다."""
|
|
46
|
+
body: dict[str, Any] = {
|
|
47
|
+
"at": at,
|
|
48
|
+
"scheduleType": schedule_type,
|
|
49
|
+
"messageType": message_type,
|
|
50
|
+
"content": content,
|
|
51
|
+
"buttons": buttons or [],
|
|
52
|
+
"image": None,
|
|
53
|
+
"imageUrl": image_url,
|
|
54
|
+
"imageLink": image_link,
|
|
55
|
+
"adFlag": ad_flag,
|
|
56
|
+
"wide": wide,
|
|
57
|
+
"adult": adult,
|
|
58
|
+
"header": header,
|
|
59
|
+
"replaceSms": replace_sms,
|
|
60
|
+
"smsSubject": sms_subject if replace_sms == "Y" else None,
|
|
61
|
+
"smsContent": sms_content if replace_sms == "Y" else None,
|
|
62
|
+
"contacts": contacts,
|
|
63
|
+
"kakaoSenderKey": self._kakao_sender_key,
|
|
64
|
+
"senderKey": self._sms_sender_key,
|
|
65
|
+
}
|
|
66
|
+
return self._http.post("friends/send", body)
|
sendgo/http_client.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from .exceptions import SendgoError
|
|
8
|
+
from .token_manager import TokenManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpClient:
|
|
12
|
+
def __init__(self, token_manager: TokenManager, base_url: str, api_version: str) -> None:
|
|
13
|
+
self._token_manager = token_manager
|
|
14
|
+
self._base_url = base_url
|
|
15
|
+
self._api_version = api_version
|
|
16
|
+
self._session = requests.Session()
|
|
17
|
+
self._session.headers.update({"Content-Type": "application/json"})
|
|
18
|
+
|
|
19
|
+
def post(self, path: str, body: dict) -> dict:
|
|
20
|
+
return self._do_post(path, body, is_retry=False)
|
|
21
|
+
|
|
22
|
+
def _do_post(self, path: str, body: dict, *, is_retry: bool) -> dict:
|
|
23
|
+
url = f"{self._base_url}/api/{self._api_version}/{path}"
|
|
24
|
+
token = self._token_manager.get_token()
|
|
25
|
+
|
|
26
|
+
resp = self._session.post(
|
|
27
|
+
url,
|
|
28
|
+
json=body,
|
|
29
|
+
headers={"Authorization": self._make_bearer(token)},
|
|
30
|
+
timeout=15,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
response_body = resp.json() if resp.content else {}
|
|
34
|
+
|
|
35
|
+
if not resp.ok:
|
|
36
|
+
error_code = response_body.get("code")
|
|
37
|
+
endpoint = path.split("/")[-1]
|
|
38
|
+
if not is_retry and self._token_manager.should_refresh(resp.status_code, error_code):
|
|
39
|
+
self._token_manager.invalidate()
|
|
40
|
+
return self._do_post(path, body, is_retry=True)
|
|
41
|
+
raise SendgoError.from_response(resp.status_code, response_body, endpoint, self._api_version)
|
|
42
|
+
|
|
43
|
+
return response_body
|
|
44
|
+
|
|
45
|
+
def _make_bearer(self, token: str) -> str:
|
|
46
|
+
if self._api_version == "v2":
|
|
47
|
+
return f"Bearer {token}"
|
|
48
|
+
return "Bearer " + base64.b64encode(token.encode()).decode()
|
sendgo/sms.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from .http_client import HttpClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SmsService:
|
|
9
|
+
"""SMS / LMS / MMS 전송 서비스.
|
|
10
|
+
|
|
11
|
+
Example::
|
|
12
|
+
|
|
13
|
+
client.sms.send_sms(content="인증번호: 123456", contacts=[{"contact": "01012345678"}])
|
|
14
|
+
client.sms.send_lms(subject="[공지]", content="...", contacts=[...])
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, http: HttpClient, sms_sender_key: str | None) -> None:
|
|
18
|
+
self._http = http
|
|
19
|
+
self._sms_sender_key = sms_sender_key
|
|
20
|
+
|
|
21
|
+
def send_sms(self, *, content: str, contacts: list[dict], **kwargs: Any) -> dict:
|
|
22
|
+
"""SMS 전송 (90자 이하)."""
|
|
23
|
+
return self.send(content=content, contacts=contacts, message_type="SMS", **kwargs)
|
|
24
|
+
|
|
25
|
+
def send_lms(self, *, content: str, contacts: list[dict], **kwargs: Any) -> dict:
|
|
26
|
+
"""LMS 전송 (장문, 2,000자 이하)."""
|
|
27
|
+
return self.send(content=content, contacts=contacts, message_type="LMS", **kwargs)
|
|
28
|
+
|
|
29
|
+
def send_mms(self, *, content: str, contacts: list[dict], **kwargs: Any) -> dict:
|
|
30
|
+
"""MMS 전송 (멀티미디어)."""
|
|
31
|
+
return self.send(content=content, contacts=contacts, message_type="MMS", **kwargs)
|
|
32
|
+
|
|
33
|
+
def send(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
content: str,
|
|
37
|
+
contacts: list[dict],
|
|
38
|
+
message_type: Literal["SMS", "LMS", "MMS"] = "SMS",
|
|
39
|
+
campaign_type: Literal["MESSAGE", "ADVERTISE", "ELECTION"] = "MESSAGE",
|
|
40
|
+
schedule_type: Literal["DIRECTLY", "SCHEDULED"] = "DIRECTLY",
|
|
41
|
+
at: str | None = None,
|
|
42
|
+
subject: str | None = None,
|
|
43
|
+
files: list | None = None,
|
|
44
|
+
) -> dict:
|
|
45
|
+
"""문자 메시지 전송."""
|
|
46
|
+
body: dict[str, Any] = {
|
|
47
|
+
"campaignType": campaign_type,
|
|
48
|
+
"messageType": message_type,
|
|
49
|
+
"scheduleType": schedule_type,
|
|
50
|
+
"at": at,
|
|
51
|
+
"subject": subject,
|
|
52
|
+
"content": content,
|
|
53
|
+
"files": files or [],
|
|
54
|
+
"contacts": contacts,
|
|
55
|
+
"senderKey": self._sms_sender_key,
|
|
56
|
+
}
|
|
57
|
+
return self._http.post("messages/send", body)
|
sendgo/token_manager.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .exceptions import SendgoError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
_NO_REFRESH_CODES = frozenset({
|
|
16
|
+
"INVALID_AUTH_HEADER", "INVALID_BASIC_AUTH", "INVALID_BASIC_AUTH_PAYLOAD",
|
|
17
|
+
"INVALID_ACCESS_KEY", "INVALID_SECRET_KEY", "ACCESS_KEY_NOT_APPROVED",
|
|
18
|
+
"TEAM_REQUIRED_FOR_KAKAO", "IP_NOT_ALLOWED", "INVALID_SENDER_KEY", "INVALID_KAKAO_SENDER_KEY",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
_TOKEN_TTL = 50 * 60 # 50분
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TokenManager:
|
|
25
|
+
"""토큰 발급 및 50분 캐시 관리."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str, access_key: str, secret_key: str, api_version: str) -> None:
|
|
28
|
+
self._base_url = base_url
|
|
29
|
+
self._access_key = access_key
|
|
30
|
+
self._secret_key = secret_key
|
|
31
|
+
self._api_version = api_version
|
|
32
|
+
self._token: str | None = None
|
|
33
|
+
self._expires_at: float = 0.0
|
|
34
|
+
self._lock = threading.Lock()
|
|
35
|
+
|
|
36
|
+
def get_token(self) -> str:
|
|
37
|
+
with self._lock:
|
|
38
|
+
if self._token and time.monotonic() < self._expires_at:
|
|
39
|
+
return self._token
|
|
40
|
+
return self._fetch_token()
|
|
41
|
+
|
|
42
|
+
def invalidate(self) -> None:
|
|
43
|
+
with self._lock:
|
|
44
|
+
self._token = None
|
|
45
|
+
self._expires_at = 0.0
|
|
46
|
+
|
|
47
|
+
def should_refresh(self, status: int, error_code: str | None) -> bool:
|
|
48
|
+
if status not in (401, 403):
|
|
49
|
+
return False
|
|
50
|
+
if self._api_version == "v2" and error_code in _NO_REFRESH_CODES:
|
|
51
|
+
return False
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def _fetch_token(self) -> str:
|
|
55
|
+
url = f"{self._base_url}/api/{self._api_version}/token"
|
|
56
|
+
credentials = base64.b64encode(f"{self._access_key}:{self._secret_key}".encode()).decode()
|
|
57
|
+
|
|
58
|
+
resp = requests.post(url, headers={
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"Authorization": f"Basic {credentials}",
|
|
61
|
+
}, timeout=10)
|
|
62
|
+
|
|
63
|
+
body = resp.json() if resp.content else {}
|
|
64
|
+
|
|
65
|
+
if not resp.ok or not body.get("data", {}).get("token"):
|
|
66
|
+
raise SendgoError.from_response(resp.status_code, body, "token", self._api_version)
|
|
67
|
+
|
|
68
|
+
self._token = body["data"]["token"]
|
|
69
|
+
self._expires_at = time.monotonic() + _TOKEN_TTL
|
|
70
|
+
return self._token
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sendgo-python
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Sendgo Python SDK — 카카오 알림톡/친구톡, SMS/LMS/MMS
|
|
5
|
+
Author-email: Sendgo <dev@sendgo.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://sendgo.io
|
|
8
|
+
Project-URL: Repository, https://github.com/sendgo-dev/sendgo-python
|
|
9
|
+
Keywords: sendgo,kakao,alimtalk,sms,notification,korea
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.28.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.9; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
25
|
+
Provides-Extra: async
|
|
26
|
+
Requires-Dist: httpx>=0.27; extra == "async"
|
|
27
|
+
|
|
28
|
+
# sendgo-python
|
|
29
|
+
|
|
30
|
+
> **Python / Django / FastAPI에서 카카오 알림톡, 친구톡, SMS를 가장 쉽게 발송하는 SDK**
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/sendgo-python/)
|
|
33
|
+
[](https://python.org)
|
|
34
|
+
[](https://pypi.org/project/sendgo-python/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
`sendgo-python`은 [Sendgo](https://sendgo.io) 알림 API를 위한 공식 Python SDK입니다.
|
|
38
|
+
**`requests` 하나만 의존하며**, 완전한 타입 힌트(Type Hints)를 제공합니다.
|
|
39
|
+
Django, FastAPI, Flask, Celery 등 모든 Python 환경에서 사용할 수 있습니다.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 목차
|
|
44
|
+
|
|
45
|
+
- [Sendgo란?](#sendgo란)
|
|
46
|
+
- [주요 기능](#주요-기능)
|
|
47
|
+
- [설치](#설치)
|
|
48
|
+
- [빠른 시작](#빠른-시작)
|
|
49
|
+
- [상세 사용법](#상세-사용법)
|
|
50
|
+
- [카카오 알림톡](#카카오-알림톡)
|
|
51
|
+
- [카카오 친구톡](#카카오-친구톡)
|
|
52
|
+
- [SMS / LMS / MMS](#sms--lms--mms)
|
|
53
|
+
- [프레임워크 통합](#프레임워크-통합)
|
|
54
|
+
- [Django](#django)
|
|
55
|
+
- [FastAPI](#fastapi)
|
|
56
|
+
- [Celery 비동기 발송](#celery-비동기-발송)
|
|
57
|
+
- [예외 처리](#예외-처리)
|
|
58
|
+
- [설정 옵션](#설정-옵션)
|
|
59
|
+
- [자주 묻는 질문](#자주-묻는-질문-faq)
|
|
60
|
+
- [관련 패키지](#관련-패키지)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Sendgo란?
|
|
65
|
+
|
|
66
|
+
[Sendgo](https://sendgo.io)는 대한민국 기업과 개발자를 위한 **통합 알림 발송 플랫폼**입니다.
|
|
67
|
+
|
|
68
|
+
- **카카오 알림톡**: 카카오톡 채널을 통한 정보성 메시지 (주문 확인, 배송 안내, 예약 확인 등)
|
|
69
|
+
- **카카오 친구톡**: 마케팅/이벤트 메시지 (쿠폰, 프로모션 등)
|
|
70
|
+
- **SMS / LMS / MMS**: 전통적인 문자 메시지
|
|
71
|
+
- **자동 대체 발송**: 알림톡 실패 시 SMS로 자동 전환
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 주요 기능
|
|
76
|
+
|
|
77
|
+
| 기능 | 설명 |
|
|
78
|
+
|------|------|
|
|
79
|
+
| **최소 의존성** | `requests` 하나만 필요 |
|
|
80
|
+
| **완전한 타입 힌트** | 모든 파라미터와 반환값에 타입 정의 |
|
|
81
|
+
| **스레드 안전 토큰 관리** | `threading.Lock` 기반, 멀티스레드 환경 안전 |
|
|
82
|
+
| **토큰 자동 캐싱(50분)** | 매 요청마다 토큰을 발급하지 않음 |
|
|
83
|
+
| **401/403 자동 재시도** | 토큰 만료 시 자동 갱신 후 재발송 |
|
|
84
|
+
| **다건 동시 발송** | 수신자 리스트로 대량 발송 |
|
|
85
|
+
| **예약 발송** | 원하는 시각에 발송 예약 |
|
|
86
|
+
| **SMS 자동 대체 발송** | 알림톡 실패 시 SMS로 자동 전환 |
|
|
87
|
+
| **v1 / v2 API 지원** | 설정 한 줄로 버전 전환 |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 설치
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install sendgo-python
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
또는 `pyproject.toml`:
|
|
98
|
+
```toml
|
|
99
|
+
[project]
|
|
100
|
+
dependencies = ["sendgo-python>=1.0.0"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 빠른 시작
|
|
106
|
+
|
|
107
|
+
### 1단계 — 환경변수 설정
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# .env
|
|
111
|
+
SENDGO_ACCESS_KEY=your_access_key
|
|
112
|
+
SENDGO_SECRET_KEY=your_secret_key
|
|
113
|
+
SENDGO_KAKAO_SENDER_KEY=your_kakao_key
|
|
114
|
+
SENDGO_SMS_SENDER_KEY=your_sms_key
|
|
115
|
+
SENDGO_API_VERSION=v2
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2단계 — 클라이언트 초기화
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import os
|
|
122
|
+
from sendgo import Sendgo
|
|
123
|
+
|
|
124
|
+
client = Sendgo(
|
|
125
|
+
access_key=os.environ["SENDGO_ACCESS_KEY"],
|
|
126
|
+
secret_key=os.environ["SENDGO_SECRET_KEY"],
|
|
127
|
+
kakao_sender_key=os.environ.get("SENDGO_KAKAO_SENDER_KEY"),
|
|
128
|
+
sms_sender_key=os.environ.get("SENDGO_SMS_SENDER_KEY"),
|
|
129
|
+
api_version="v2",
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3단계 — 알림톡 전송
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
client.alimtalk.send(
|
|
137
|
+
template_code="ORDER_CONFIRM_001",
|
|
138
|
+
contacts=[
|
|
139
|
+
{
|
|
140
|
+
"contact": "01012345678", # 수신자 전화번호 (필수)
|
|
141
|
+
"name": "홍길동", # 수신자 이름 (선택)
|
|
142
|
+
"var1": "ORD-20260723-001", # 템플릿 변수 #{var1}
|
|
143
|
+
"var2": "스프링 부트 가이드", # 템플릿 변수 #{var2}
|
|
144
|
+
"var3": "29,000원", # 템플릿 변수 #{var3}
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 상세 사용법
|
|
153
|
+
|
|
154
|
+
### 카카오 알림톡
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# 다건 발송
|
|
158
|
+
client.alimtalk.send(
|
|
159
|
+
template_code="ORDER_CONFIRM_001",
|
|
160
|
+
contacts=[
|
|
161
|
+
{"contact": "01011111111", "name": "홍길동", "var1": "ORD-001"},
|
|
162
|
+
{"contact": "01022222222", "name": "김철수", "var1": "ORD-002"},
|
|
163
|
+
{"contact": "01033333333", "name": "이영희", "var1": "ORD-003"},
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# 예약 발송
|
|
168
|
+
client.alimtalk.send(
|
|
169
|
+
template_code="PROMO_SUMMER_2026",
|
|
170
|
+
schedule_type="SCHEDULED",
|
|
171
|
+
at="2026-07-28 09:00:00",
|
|
172
|
+
contacts=[{"contact": "01012345678", "var1": "여름 한정 50% 할인"}],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# 알림톡 실패 시 SMS 자동 대체 발송
|
|
176
|
+
client.alimtalk.send(
|
|
177
|
+
template_code="DELIVERY_START_001",
|
|
178
|
+
contacts=[{"contact": "01012345678", "var1": "ORD-001", "var2": "1234567890"}],
|
|
179
|
+
replace_sms="Y",
|
|
180
|
+
sms_subject="[배송 시작 안내]",
|
|
181
|
+
sms_content="주문하신 상품이 출고되었습니다.\n송장번호: #{var2}",
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 카카오 친구톡
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# 텍스트형
|
|
189
|
+
client.friendtalk.send(
|
|
190
|
+
content="안녕하세요! 7월 한정 특가 이벤트를 확인해보세요.",
|
|
191
|
+
contacts=[{"contact": "01012345678"}],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# 이미지형
|
|
195
|
+
client.friendtalk.send(
|
|
196
|
+
message_type="FI",
|
|
197
|
+
content="이번 주 특가 상품을 확인하세요!",
|
|
198
|
+
image_url="https://cdn.example.com/banner.jpg",
|
|
199
|
+
image_link="https://example.com/event",
|
|
200
|
+
contacts=[{"contact": "01012345678"}],
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### SMS / LMS / MMS
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
# SMS
|
|
208
|
+
client.sms.send_sms(
|
|
209
|
+
content="[Sendgo] 인증번호: 123456 (5분 이내 입력)",
|
|
210
|
+
contacts=[{"contact": "01012345678"}],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# LMS — 장문 (2,000자 이하)
|
|
214
|
+
client.sms.send_lms(
|
|
215
|
+
subject="[중요] 서비스 점검 안내",
|
|
216
|
+
content="""안녕하세요. 서비스 점검이 예정되어 있습니다.
|
|
217
|
+
|
|
218
|
+
■ 점검 일시: 2026-07-25 02:00 ~ 06:00
|
|
219
|
+
■ 영향 범위: 전체 서비스
|
|
220
|
+
|
|
221
|
+
이용에 불편을 드려 죄송합니다.""",
|
|
222
|
+
contacts=[{"contact": "01012345678"}],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# MMS — 이미지 포함
|
|
226
|
+
client.sms.send_mms(
|
|
227
|
+
subject="[이벤트] 7월 특가",
|
|
228
|
+
content="이번 달 특가 상품을 확인하세요!",
|
|
229
|
+
contacts=[{"contact": "01012345678"}],
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 프레임워크 통합
|
|
236
|
+
|
|
237
|
+
### Django
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
# settings.py
|
|
241
|
+
SENDGO = {
|
|
242
|
+
"access_key": env("SENDGO_ACCESS_KEY"),
|
|
243
|
+
"secret_key": env("SENDGO_SECRET_KEY"),
|
|
244
|
+
"kakao_sender_key": env("SENDGO_KAKAO_SENDER_KEY", default=None),
|
|
245
|
+
"sms_sender_key": env("SENDGO_SMS_SENDER_KEY", default=None),
|
|
246
|
+
"api_version": env("SENDGO_API_VERSION", default="v2"),
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
# apps/notifications/services.py
|
|
252
|
+
from django.conf import settings
|
|
253
|
+
from sendgo import Sendgo
|
|
254
|
+
|
|
255
|
+
_sendgo: Sendgo | None = None
|
|
256
|
+
|
|
257
|
+
def get_sendgo() -> Sendgo:
|
|
258
|
+
global _sendgo
|
|
259
|
+
if _sendgo is None:
|
|
260
|
+
_sendgo = Sendgo(**settings.SENDGO)
|
|
261
|
+
return _sendgo
|
|
262
|
+
|
|
263
|
+
def send_order_confirm(phone: str, order_number: str) -> None:
|
|
264
|
+
get_sendgo().alimtalk.send(
|
|
265
|
+
template_code="ORDER_CONFIRM_001",
|
|
266
|
+
contacts=[{"contact": phone, "var1": order_number}],
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
# apps/orders/signals.py
|
|
272
|
+
from django.db.models.signals import post_save
|
|
273
|
+
from django.dispatch import receiver
|
|
274
|
+
from .models import Order
|
|
275
|
+
from apps.notifications.services import send_order_confirm
|
|
276
|
+
|
|
277
|
+
@receiver(post_save, sender=Order)
|
|
278
|
+
def on_order_created(sender, instance, created, **kwargs):
|
|
279
|
+
if created:
|
|
280
|
+
send_order_confirm(instance.user.phone, instance.number)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### FastAPI
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# core/sendgo.py
|
|
287
|
+
from functools import lru_cache
|
|
288
|
+
from sendgo import Sendgo
|
|
289
|
+
from .config import settings
|
|
290
|
+
|
|
291
|
+
@lru_cache
|
|
292
|
+
def get_sendgo() -> Sendgo:
|
|
293
|
+
return Sendgo(
|
|
294
|
+
access_key=settings.SENDGO_ACCESS_KEY,
|
|
295
|
+
secret_key=settings.SENDGO_SECRET_KEY,
|
|
296
|
+
kakao_sender_key=settings.SENDGO_KAKAO_SENDER_KEY,
|
|
297
|
+
api_version="v2",
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
# routers/notify.py
|
|
303
|
+
from fastapi import APIRouter, Depends
|
|
304
|
+
from sendgo import Sendgo
|
|
305
|
+
from core.sendgo import get_sendgo
|
|
306
|
+
|
|
307
|
+
router = APIRouter(prefix="/api")
|
|
308
|
+
|
|
309
|
+
@router.post("/notify/order")
|
|
310
|
+
async def notify_order(
|
|
311
|
+
phone: str,
|
|
312
|
+
order_number: str,
|
|
313
|
+
sendgo: Sendgo = Depends(get_sendgo),
|
|
314
|
+
):
|
|
315
|
+
sendgo.alimtalk.send(
|
|
316
|
+
template_code="ORDER_CONFIRM_001",
|
|
317
|
+
contacts=[{"contact": phone, "var1": order_number}],
|
|
318
|
+
)
|
|
319
|
+
return {"success": True}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Celery 비동기 발송
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
# tasks/notifications.py
|
|
326
|
+
from celery import shared_task
|
|
327
|
+
from sendgo import Sendgo, SendgoError
|
|
328
|
+
import logging
|
|
329
|
+
|
|
330
|
+
logger = logging.getLogger(__name__)
|
|
331
|
+
|
|
332
|
+
@shared_task(bind=True, max_retries=3, default_retry_delay=10)
|
|
333
|
+
def send_alimtalk_task(self, template_code: str, contacts: list[dict]) -> None:
|
|
334
|
+
"""카카오 알림톡 비동기 발송 Celery 태스크"""
|
|
335
|
+
sendgo = Sendgo(
|
|
336
|
+
access_key=settings.SENDGO_ACCESS_KEY,
|
|
337
|
+
secret_key=settings.SENDGO_SECRET_KEY,
|
|
338
|
+
kakao_sender_key=settings.SENDGO_KAKAO_SENDER_KEY,
|
|
339
|
+
)
|
|
340
|
+
try:
|
|
341
|
+
sendgo.alimtalk.send(template_code=template_code, contacts=contacts)
|
|
342
|
+
except SendgoError as e:
|
|
343
|
+
logger.error("알림톡 발송 실패: %s [%s]", e, e.error_code)
|
|
344
|
+
if e.error_code not in ("INVALID_TEMPLATE_CODE", "PAYMENT_REQUIRED"):
|
|
345
|
+
raise self.retry(exc=e)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
# 사용
|
|
350
|
+
send_alimtalk_task.delay("ORDER_CONFIRM_001", [{"contact": "01012345678", "var1": "ORD-001"}])
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 예외 처리
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from sendgo import SendgoError
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
client.alimtalk.send(
|
|
362
|
+
template_code="ORDER_CONFIRM_001",
|
|
363
|
+
contacts=[{"contact": "01012345678"}],
|
|
364
|
+
)
|
|
365
|
+
except SendgoError as e:
|
|
366
|
+
print(f"발송 실패: HTTP {e.status_code} [{e.error_code}]")
|
|
367
|
+
print(f"엔드포인트: {e.endpoint}, API 버전: {e.api_version}")
|
|
368
|
+
|
|
369
|
+
match e.error_code:
|
|
370
|
+
case "INVALID_ACCESS_KEY" | "INVALID_SECRET_KEY":
|
|
371
|
+
alert_ops("Sendgo 인증키를 확인하세요.")
|
|
372
|
+
case "INVALID_TEMPLATE_CODE":
|
|
373
|
+
logger.warning("존재하지 않는 템플릿: %s", template_code)
|
|
374
|
+
case "PAYMENT_REQUIRED":
|
|
375
|
+
alert_ops("Sendgo 크레딧이 부족합니다.")
|
|
376
|
+
case "IP_NOT_ALLOWED":
|
|
377
|
+
alert_ops("허용되지 않은 IP에서 요청이 발생했습니다.")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 설정 옵션
|
|
383
|
+
|
|
384
|
+
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|
|
385
|
+
|---------|------|------|--------|------|
|
|
386
|
+
| `access_key` | `str` | **필수** | — | Sendgo 액세스 키 |
|
|
387
|
+
| `secret_key` | `str` | **필수** | — | Sendgo 시크릿 키 |
|
|
388
|
+
| `kakao_sender_key` | `str \| None` | 선택 | `None` | 카카오 발신프로필 키 |
|
|
389
|
+
| `sms_sender_key` | `str \| None` | 선택 | `None` | SMS 발신자 키 |
|
|
390
|
+
| `api_version` | `str` | 선택 | `'v1'` | API 버전 (`v1` \| `v2`) |
|
|
391
|
+
| `base_url` | `str` | 선택 | `'https://sendgo.io'` | API 기본 URL |
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 자주 묻는 질문 (FAQ)
|
|
396
|
+
|
|
397
|
+
**Q. 비동기(async/await)를 지원하나요?**
|
|
398
|
+
A. 현재 버전은 동기(`requests` 기반)만 지원합니다. FastAPI 등 비동기 환경에서는 `asyncio.get_event_loop().run_in_executor()`로 스레드풀에서 실행하거나, Celery 태스크로 위임하는 방법을 권장합니다. 비동기 버전(`httpx` 기반)은 향후 추가될 예정입니다.
|
|
399
|
+
|
|
400
|
+
**Q. 멀티스레드 환경에서 안전한가요?**
|
|
401
|
+
A. 토큰 관리에 `threading.Lock`을 사용하여 멀티스레드 환경에서도 안전합니다.
|
|
402
|
+
|
|
403
|
+
**Q. 알림톡 템플릿은 어디서 등록하나요?**
|
|
404
|
+
A. [Sendgo 콘솔](https://sendgo.io) → 알림톡 템플릿 → 템플릿 작성 → 카카오 심사 신청 (보통 1~3일 소요)
|
|
405
|
+
|
|
406
|
+
**Q. 대량 발송 시 rate limit이 있나요?**
|
|
407
|
+
A. Sendgo 플랜별로 TPS 제한이 있습니다. [요금 정책](https://sendgo.io/pricing) 참조.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 관련 패키지
|
|
412
|
+
|
|
413
|
+
| 언어/프레임워크 | 패키지 | GitHub |
|
|
414
|
+
|----------------|--------|--------|
|
|
415
|
+
| Spring Boot | `io.sendgo:sendgo-spring` | [sendgo-spring-boot-starter](https://github.com/send-go/spring) |
|
|
416
|
+
| Node.js | `@sendgo/node` | [sendgo-node](https://github.com/send-go/node) |
|
|
417
|
+
| Go | `github.com/send-go/go` | [sendgo-go](https://github.com/send-go/go) |
|
|
418
|
+
| 전체 목록 | — | [send-go GitHub 조직](https://github.com/send-go) |
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## 라이선스
|
|
423
|
+
|
|
424
|
+
MIT License © 2026 [Sendgo](https://sendgo.io)
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
*키워드: 카카오 알림톡 Python, 카카오 친구톡 Django, SMS 발송 FastAPI, 알림톡 SDK pip, Python 카카오 API 연동, Django 문자 발송, FastAPI 알림톡, Celery 알림톡 비동기, Sendgo Python SDK*
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
sendgo/__init__.py,sha256=1dUImXkaop7KOI-u8IkpoY3AtCUYs-OBrEnq1YujQDc,592
|
|
2
|
+
sendgo/alimtalk.py,sha256=2dKctTPqn18Ev14PGoiSYtdHvoMvkwDWL0AulgIG3sU,1959
|
|
3
|
+
sendgo/client.py,sha256=t0rpnY7qfLv0luP1ywhtvKRLP8eeauTTYMu-Mfph4Hc,1538
|
|
4
|
+
sendgo/exceptions.py,sha256=EiBZJPp72pwwMA3lR6cSatvaxa4ooBTvf8bk9XL8g50,1423
|
|
5
|
+
sendgo/friendtalk.py,sha256=XyWxaJD92TbXXdcrKwhruU7WtiUeFORmBhv8RrXwCdw,2192
|
|
6
|
+
sendgo/http_client.py,sha256=KBwQMig59521ikQFR-ZEdO23Gr1xkPM4Dg_aU7CxNJE,1685
|
|
7
|
+
sendgo/sms.py,sha256=546qRFafKLd5W4WAO1K4xRJj7PYtYfevPdN7qWWiZlc,2073
|
|
8
|
+
sendgo/token_manager.py,sha256=xhiKimI_30nkSrdPfkQwT9ksu6jbZtjzmtaOjUX2hkc,2244
|
|
9
|
+
sendgo_python-1.0.1.dist-info/METADATA,sha256=dP8EBrPqSDdnpgBDd7hHU6ccc6ZmM8mLxO3PeM5jGjE,13256
|
|
10
|
+
sendgo_python-1.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
sendgo_python-1.0.1.dist-info/top_level.txt,sha256=1_OCirphGoTnHC9W3nkzSzNMPLypnQOG9YQYqUZ4E2g,7
|
|
12
|
+
sendgo_python-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sendgo
|