Skip to content

Commit 8491fd9

Browse files
Change directory structure and Add files
1 parent 75d5e7e commit 8491fd9

21 files changed

Lines changed: 917 additions & 91 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Solapi SDK for Python
22

3-
[![Python 3.10.2](https://img.shields.io/badge/python-3.13.2-blue.svg)](https://www.python.org/downloads)
4-
![Python Supported version](https://img.shields.io/badge/python-%3E%3D3.11-orange)
3+
[![Python 3.13.2](https://img.shields.io/badge/python-3.13.2-blue.svg)](https://www.python.org/downloads)
4+
![Python Supported version](https://img.shields.io/badge/python-%3E%3D3.9-orange)

examples/sms/sms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
print("test")
1+
print("test")

poetry.lock

Lines changed: 274 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ authors = [
66
{ name = "SOLAPI Team", email = "contact@solapi.com" }
77
]
88
readme = "README.md"
9-
requires-python = ">=3.11"
9+
requires-python = ">=3.9"
1010
dependencies = [
11-
"requests (>=2.32.3,<3.0.0)"
11+
"requests (>=2.32.3,<3.0.0)",
12+
"httpx (>=0.28.1,<0.29.0)",
13+
"pydantic (>=2.10.6,<3.0.0)"
1214
]
1315

1416
[tool.poetry]

solapi/lib/authenticator.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import datetime
2+
import hashlib
3+
import hmac
4+
import time
5+
import uuid
6+
from typing import TypedDict
7+
8+
9+
class AuthenticationParameter(TypedDict):
10+
api_key: str
11+
api_secret: str
12+
13+
14+
class Authenticator:
15+
def __init__(self, api_key="", api_secret_key=""):
16+
self.api_key = api_key
17+
self.api_secret_key = api_secret_key
18+
19+
@staticmethod
20+
def unique_id() -> str:
21+
return uuid.uuid1().hex
22+
23+
@staticmethod
24+
def get_iso_datetime() -> str:
25+
utc_offset_sec = time.altzone if time.localtime().tm_isdst else time.timezone
26+
utc_offset = datetime.timedelta(seconds=-utc_offset_sec)
27+
return (
28+
datetime.datetime.now()
29+
.replace(tzinfo=datetime.timezone(offset=utc_offset))
30+
.isoformat()
31+
)
32+
33+
@staticmethod
34+
def get_signature(key: str, msg: str) -> str:
35+
return hmac.new(key.encode(), msg.encode(), hashlib.sha256).hexdigest()
36+
37+
def get_auth_info(self) -> str:
38+
date = self.get_iso_datetime()
39+
salt = self.unique_id()
40+
data = date + salt
41+
signature = self.get_signature(self.api_secret_key, data)
42+
return f"HMAC-SHA256 ApiKey={self.api_key}, Date={date}, salt={salt}, signature={signature}"

solapi/lib/fetcher.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from enum import Enum
2+
from typing import Any, TypedDict, TypeVar
3+
4+
import httpx
5+
from httpx import Response
6+
7+
from solapi.lib.authenticator import AuthenticationParameter, Authenticator
8+
9+
10+
class RequestMethod(str, Enum):
11+
GET = "GET"
12+
POST = "POST"
13+
PUT = "PUT"
14+
DELETE = "DELETE"
15+
16+
17+
class RequestType(TypedDict):
18+
method: RequestMethod
19+
url: str
20+
21+
22+
T = TypeVar("T")
23+
R = TypeVar("R")
24+
25+
26+
async def default_fetcher(
27+
auth_parameter: AuthenticationParameter,
28+
request: RequestType,
29+
data: T | None = None,
30+
) -> R:
31+
"""
32+
33+
Args:
34+
auth_parameter: API Key, Api Secret Key Dictionary. 반드시 활성화된 유효한 API Key, API Secret Key를 입력하셔야 합니다.
35+
request: HTTP Request를 위한 파라미터, 정확한 API URL, HTTP Method를 입력해주세요!
36+
data: 발송 혹은 API 요청 시 전달해야 할 파라미터 목록, HTTP GET API를 이용하면 None 타입으로 지정됩니다.
37+
38+
Returns:
39+
정상 작동할 경우 JSON 데이터를 반환합니다.
40+
"""
41+
authorization_header_data = Authenticator(
42+
auth_parameter["api_key"], auth_parameter["api_secret"]
43+
).get_auth_info()
44+
headers = {
45+
"Authorization": authorization_header_data,
46+
"Content-Type": "application/json",
47+
}
48+
49+
async with httpx.AsyncClient() as client:
50+
response: Response = await client.request(
51+
method=request["method"], url=request["url"], headers=headers, json=data
52+
)
53+
54+
# 4xx 에러 처리: 클라이언트 오류일 경우
55+
# TODO: Custom Error 만들어야 함
56+
if 400 <= response.status_code < 500:
57+
error_response: dict[str, Any] = response.json()
58+
raise Exception(
59+
error_response.get("errorCode", "UnknownError"),
60+
error_response.get("errorMessage", ""),
61+
)
62+
# 5xx 에러 처리: 서버 오류일 경우
63+
elif response.status_code >= 500:
64+
raise Exception("UnknownError", response.text)
65+
66+
try:
67+
return response.json()
68+
except Exception as exc:
69+
raise Exception(response.text) from exc
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import re
22
from datetime import datetime
3-
from typing import Union
43

54

65
class InvalidDateError(Exception):
@@ -61,7 +60,7 @@ def parse_iso(date_string: str) -> datetime:
6160
raise InvalidDateError("Invalid Date") from e
6261

6362

64-
def string_date_transfer(value: Union[str, datetime]) -> datetime:
63+
def string_date_transfer(value: str | datetime) -> datetime:
6564
"""
6665
일반 문자열 날짜가 있을 경우 datetime 타입으로 변환해주는 함수
6766
@@ -83,7 +82,7 @@ def string_date_transfer(value: Union[str, datetime]) -> datetime:
8382
return value
8483

8584

86-
def transfer(value: Union[str, datetime]) -> str:
85+
def format_with_transfer(value: str | datetime) -> str:
8786
"""
8887
string_date_transfer와 format_iso를 한번에 실행하는 함수
8988

solapi/message_service.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from typing import Optional
2+
3+
from solapi.lib.authenticator import AuthenticationParameter
4+
from solapi.lib.fetcher import RequestMethod, default_fetcher
5+
from solapi.lib.string_date_transfer import format_with_transfer
6+
from solapi.requests.request_confiog import SendRequestConfig
7+
from solapi.requests.send.send_request import (
8+
MessageParameter,
9+
MultipleDetailMessageSendingRequest,
10+
)
11+
from solapi.responses.send.send_repsonse import DetailGroupMessageResponse
12+
13+
14+
class SolapiMessageService:
15+
def __init__(self, api_key: str, api_secret: str):
16+
self.baseUrl = "https://api.solapi.com"
17+
self._auth_info: AuthenticationParameter = {
18+
"api_key": api_key,
19+
"api_secret": api_secret,
20+
}
21+
22+
# TODO: Request도 pydantic 모델로 진행
23+
async def send(
24+
self,
25+
messages: list[MessageParameter] | MessageParameter,
26+
request_config: Optional[SendRequestConfig] = None,
27+
):
28+
payload = []
29+
if isinstance(messages, list):
30+
for value in messages:
31+
payload.append(value)
32+
else:
33+
payload.append(messages)
34+
pass
35+
36+
if len(payload) == 0:
37+
raise Exception("데이터가 반드시 1건 이상 기입되어 있어야 합니다.")
38+
39+
parameter: MultipleDetailMessageSendingRequest = {"messages": payload}
40+
if request_config["appId"] is not None:
41+
parameter.appId = request_config["appId"]
42+
if request_config["showMessageList"] is not None:
43+
parameter.showMessageList = request_config["showMessageList"]
44+
if request_config["scheduledDate"] is not None:
45+
parameter.scheduledDate = format_with_transfer(
46+
request_config["scheduledDate"]
47+
)
48+
49+
response = await default_fetcher(
50+
self._auth_info,
51+
{
52+
"method": RequestMethod("POST"),
53+
"url": f"${self.baseUrl}/messages/v4/send-many/detail",
54+
},
55+
parameter,
56+
)
57+
validated_response = DetailGroupMessageResponse.model_validate(response)
58+
count = validated_response.group_info.count
59+
if (
60+
len(validated_response.failed_message_list) > 0
61+
and count.total == count.registeredFailed
62+
):
63+
raise Exception("")
64+
65+
return validated_response
66+

0 commit comments

Comments
 (0)