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 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)
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/sendgo-python?logo=pypi)](https://pypi.org/project/sendgo-python/)
33
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python)](https://python.org)
34
+ [![Downloads](https://img.shields.io/pypi/dm/python)](https://pypi.org/project/sendgo-python/)
35
+ [![License](https://img.shields.io/badge/license-MIT-blue)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sendgo