Skip to content

Commit 718ca51

Browse files
Update pytest code, Add cancel_scheduled_message method in message_service.py, Fix errors in rcs_options.py
1 parent 4cf68b5 commit 718ca51

File tree

9 files changed

+125
-87
lines changed

9 files changed

+125
-87
lines changed

solapi/lib/string_date_transfer.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,15 @@ def string_date_transfer(value: Union[str, datetime]):
8080
if isinstance(value, str):
8181
try:
8282
value = parse_iso(value)
83-
except Exception as e:
84-
raise InvalidDateError("Invalid Date") from e
85-
83+
except InvalidDateError: # parse_iso가 실패한 경우
84+
try:
85+
# "YYYY-MM-DD HH:MM:SS" 형식 시도
86+
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
87+
except ValueError as e:
88+
# 두 형식 모두 실패한 경우 원래 에러 발생
89+
raise InvalidDateError(
90+
"Invalid Date format. Expected ISO 8601 or YYYY-MM-DD HH:MM:SS"
91+
) from e
8692
return value
8793

8894

solapi/model/rcs/rcs_options.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22
from enum import Enum
3-
from typing import Any, Optional
3+
from typing import Optional
44

55
from pydantic import BaseModel, ConfigDict
66
from pydantic.alias_generators import to_camel
@@ -27,15 +27,6 @@ def __repr__(self):
2727
return repr(self.value)
2828

2929

30-
class RcsAdditionalBody(BaseModel):
31-
title: str
32-
description: str
33-
image_id: Optional[str] = None
34-
buttons: Optional[dict[str, str]] = None
35-
36-
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
37-
38-
3930
class RcsButtonType(str, Enum):
4031
"""
4132
'WL'(웹링크), 'ML'(지도[좌표]), 'MQ'(지도[쿼리]), 'MR'(위치공유),
@@ -74,6 +65,15 @@ class RcsButton(BaseModel):
7465
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
7566

7667

68+
class RcsAdditionalBody(BaseModel):
69+
title: str
70+
description: str
71+
image_id: Optional[str] = None
72+
buttons: Optional[list[RcsButton]] = None
73+
74+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
75+
76+
7777
class RcsOption(BaseModel):
7878
brand_id: Optional[str] = None
7979
template_id: Optional[str] = None
@@ -82,8 +82,7 @@ class RcsOption(BaseModel):
8282
mms_type: Optional[RcsMmsType] = None
8383
commercial_type: Optional[bool] = None
8484
disable_sms: Optional[bool] = False
85-
additional_body: Optional[Any] = None
86-
# additional_body: Optional[list[RcsAdditionalBody]] = None
85+
additional_body: Optional[list[RcsAdditionalBody]] = None
8786
buttons: Optional[list[RcsButton]] = None
8887

8988
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

solapi/model/request/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# NOTE: Python SDK가 업데이트 될 때마다 Version도 갱신해야 함!
2-
VERSION = "python/5.0.0"
2+
VERSION = "python/5.0.1"
Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22
from typing import Optional, Union
33

4-
from pydantic import BaseModel, ConfigDict, Field, computed_field
4+
from pydantic import BaseModel, ConfigDict, Field, field_validator
55
from pydantic.alias_generators import to_camel
66

77
from solapi.lib.string_date_transfer import format_with_transfer
@@ -24,18 +24,11 @@ class GetMessagesRequest(BaseModel):
2424

2525
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
2626

27-
@computed_field(alias="startDate")
28-
@property
29-
def formatted_start_date(self) -> Union[str, None]:
30-
if self.start_date is not None:
31-
return format_with_transfer(self.start_date)
32-
else:
33-
return None
34-
35-
@computed_field(alias="endDate")
36-
@property
37-
def formatted_end_date(self) -> Union[str, None]:
38-
if self.end_date is not None:
39-
return format_with_transfer(self.end_date)
40-
else:
41-
return None
27+
@field_validator("start_date", "end_date", mode="before")
28+
@classmethod
29+
def format_dates(
30+
cls, value: Union[str, datetime, None]
31+
) -> Union[str, datetime, None]:
32+
if isinstance(value, str):
33+
return format_with_transfer(value)
34+
return value

solapi/services/message_service.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ def send(
8888
)
8989

9090
payload.extend(messages)
91-
else:
92-
raise TypeError("Invalid message type")
9391

9492
if len(payload) == 0:
9593
raise ValueError("The data must have at least one message.")
@@ -168,6 +166,21 @@ def upload_file(
168166
)
169167
return FileUploadResponse.model_validate(response)
170168

169+
def cancel_scheduled_message(self, group_id: str) -> GroupMessageResponse:
170+
"""Cancel a reserved message.
171+
172+
This method cancels a message that was previously scheduled to be sent.
173+
It requires the group_id of the message to be canceled.
174+
"""
175+
response = default_fetcher(
176+
self.auth_info,
177+
request={
178+
"url": f"{self.base_url}/messages/v4/groups/{group_id}/schedule",
179+
"method": RequestMethod.DELETE,
180+
},
181+
)
182+
return GroupMessageResponse.model_validate(response)
183+
171184
def get_groups(self, query: Optional[GetGroupsRequest] = None) -> GetGroupsResponse:
172185
"""Retrieve message groups (메시지 그룹) from the Solapi API.
173186

tests/test_group.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
from solapi.model.response.groups.get_group_messages import GetGroupMessagesResponse
44
from solapi.model.response.groups.get_groups import GetGroupsResponse
5+
from solapi.services.message_service import SolapiMessageService
56

67

78
class TestGroup:
89
"""Test cases for group-related functionality."""
910

10-
def test_get_groups(self, message_service):
11+
def test_get_groups(self, message_service: SolapiMessageService):
1112
"""
1213
Test getting message groups.
1314
@@ -41,7 +42,7 @@ def test_get_groups(self, message_service):
4142
# Test get_group method with this group ID
4243
self.test_get_group(message_service, group_id)
4344

44-
def test_get_group(self, message_service, group_id=None):
45+
def test_get_group(self, message_service: SolapiMessageService, group_id=None):
4546
"""
4647
Test getting a specific message group.
4748
@@ -92,15 +93,15 @@ def test_get_group_messages(self, message_service, group_id=None):
9293
assert isinstance(response, GetGroupMessagesResponse)
9394

9495
# Verify response has required fields
95-
assert hasattr(response, "messages")
96+
assert hasattr(response, "message_list")
9697

9798
# Print message information for verification
98-
print(f"Number of messages in group: {len(response.messages)}")
99+
print(f"Number of messages in group: {len(response.message_list)}")
99100

100101
# If there are messages, verify the structure of the first message
101-
if response.messages:
102-
message = response.messages[0]
103-
print(f"Message ID: {message.message_id}")
104-
print(f"Status: {message.status}")
105-
print(f"To: {message.to}")
106-
print(f"From: {message.from_}")
102+
if response.message_list:
103+
for message in response.message_list.values():
104+
print(f"Message ID: {message.message_id}")
105+
print(f"Status: {message.status_code}")
106+
print(f"To: {message.to}")
107+
print(f"From: {message.from_}")

tests/test_messages.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,18 @@ def test_get_messages(self, message_service):
2424
assert isinstance(response, GetMessagesResponse)
2525

2626
# Verify response has required fields
27-
assert hasattr(response, "messages")
28-
assert hasattr(response, "total_count")
27+
assert hasattr(response, "message_list")
2928

3029
# Print message information for verification
31-
print(f"Total messages: {response.total_count}")
32-
print(f"Messages in response: {len(response.messages)}")
30+
print(f"Messages in response: {len(response.message_list)}")
3331

3432
# If there are messages, verify the structure of the first message
35-
if response.messages:
36-
message = response.messages[0]
37-
print(f"Message ID: {message.message_id}")
38-
print(f"Status: {message.status}")
39-
print(f"To: {message.to}")
40-
print(f"From: {message.from_}")
33+
if response.message_list:
34+
for message in response.message_list.values():
35+
print(f"Message ID: {message.message_id}")
36+
print(f"Status: {message.status_code}")
37+
print(f"To: {message.to}")
38+
print(f"From: {message.from_}")
4139

4240
def test_get_messages_with_date_filter(self, message_service):
4341
"""
@@ -67,20 +65,15 @@ def test_get_messages_with_date_filter(self, message_service):
6765
assert isinstance(response, GetMessagesResponse)
6866

6967
# Verify response has required fields
70-
assert hasattr(response, "messages")
71-
assert hasattr(response, "total_count")
68+
assert hasattr(response, "message_list")
7269

7370
# Print message information for verification
74-
print(
75-
f"Total messages in date range {start_date_str} to {end_date_str}: {response.total_count}"
76-
)
77-
print(f"Messages in response: {len(response.messages)}")
71+
print(f"Messages in response: {len(response.message_list)}")
7872

7973
# If there are messages, verify the structure of the first message
80-
if response.messages:
81-
message = response.messages[0]
82-
print(f"Message ID: {message.message_id}")
83-
print(f"Status: {message.status}")
84-
print(f"To: {message.to}")
85-
print(f"From: {message.from_}")
86-
print(f"Date: {message.date_created}")
74+
if response.message_list:
75+
for message in response.message_list.values():
76+
print(f"Message ID: {message.message_id}")
77+
print(f"Status: {message.status_code}")
78+
print(f"To: {message.to}")
79+
print(f"From: {message.from_}")

tests/test_simple_send.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from solapi.model.kakao.kakao_option import KakaoOption
88
from solapi.model.request.storage import FileTypeEnum
99
from solapi.model.response.send_message_response import SendMessageResponse
10+
from solapi.services.message_service import SolapiMessageService
1011

1112

1213
class TestSimpleSend:
@@ -51,7 +52,7 @@ def test_send_sms(self, message_service, test_phone_numbers):
5152
print(f"Successful messages: {response.group_info.count.registered_success}")
5253
print(f"Failed messages: {response.group_info.count.registered_failed}")
5354

54-
return response.group_info.group_id
55+
assert response.group_info.group_id is not None
5556

5657
def test_send_mms(self, message_service, test_phone_numbers):
5758
"""
@@ -113,7 +114,7 @@ def test_send_mms(self, message_service, test_phone_numbers):
113114
print(f"Successful messages: {response.group_info.count.registered_success}")
114115
print(f"Failed messages: {response.group_info.count.registered_failed}")
115116

116-
return response.group_info.group_id
117+
assert response.group_info.group_id is not None
117118

118119
def test_send_kakao_alimtalk(
119120
self, message_service, test_phone_numbers, test_kakao_options
@@ -166,7 +167,7 @@ def test_send_kakao_alimtalk(
166167
)
167168
print(f"Failed messages: {response.group_info.count.registered_failed}")
168169

169-
return response.group_info.group_id
170+
assert response.group_info.group_id is not None
170171
except Exception as e:
171172
# This test may fail if Kakao template is not properly set up
172173
pytest.skip(f"Kakao Alimtalk test skipped: {str(e)}")
@@ -232,7 +233,7 @@ def test_send_many(self, message_service, test_phone_numbers):
232233
print(f"To: {failed.message.to}")
233234
print(f"Error: {failed.error.message}")
234235

235-
return response.group_info.group_id
236+
assert response.group_info.group_id is not None
236237

237238
def test_send_with_reservation(self, message_service, test_phone_numbers):
238239
"""
@@ -250,14 +251,11 @@ def test_send_with_reservation(self, message_service, test_phone_numbers):
250251
from_=test_phone_numbers["sender"],
251252
to=test_phone_numbers["recipient"],
252253
text="[테스트] SOLAPI Python SDK를 사용한 예약 발송 테스트입니다.",
254+
scheduled_date=datetime.now() + timedelta(minutes=10),
253255
)
254256

255-
# Create config with scheduled date (10 minutes in the future)
256-
scheduled_date = datetime.now() + timedelta(minutes=10)
257-
config = SendRequestConfig(scheduled_date=scheduled_date)
258-
259-
# Send message with reservation
260-
response = message_service.send(message, config)
257+
# Send message
258+
response = message_service.send(message)
261259

262260
# Verify response type
263261
assert isinstance(response, SendMessageResponse)
@@ -267,15 +265,50 @@ def test_send_with_reservation(self, message_service, test_phone_numbers):
267265
assert hasattr(response.group_info, "group_id")
268266
assert hasattr(response.group_info, "count")
269267

270-
# Verify message was scheduled successfully
268+
# Verify message was sent successfully
271269
assert response.group_info.count.total > 0
272270
assert response.group_info.count.registered_success > 0
273271

274272
# Print response information for verification
275273
print(f"Group ID: {response.group_info.group_id}")
276-
print(f"Scheduled date: {scheduled_date}")
277274
print(f"Total messages: {response.group_info.count.total}")
278275
print(f"Successful messages: {response.group_info.count.registered_success}")
279276
print(f"Failed messages: {response.group_info.count.registered_failed}")
280277

281-
return response.group_info.group_id
278+
assert response.group_info.group_id is not None
279+
280+
def test_cancel_reservation(
281+
self, message_service: SolapiMessageService, test_phone_numbers
282+
):
283+
"""
284+
Test cancelling a reserved message.
285+
286+
This test verifies that the cancel_scheduled_message method works correctly.
287+
288+
Args:
289+
message_service: The SolapiMessageService fixture
290+
test_phone_numbers: Dictionary with sender and recipient phone numbers
291+
"""
292+
# Create message with reservation
293+
message = RequestMessage(
294+
from_=test_phone_numbers["sender"],
295+
to=test_phone_numbers["recipient"],
296+
text="[테스트] SOLAPI Python SDK 예약 취소 테스트입니다.",
297+
)
298+
request_config = SendRequestConfig(
299+
scheduled_date=datetime.now() + timedelta(minutes=10)
300+
)
301+
302+
# Send message and get group_id
303+
send_response = message_service.send(message, request_config)
304+
group_id = send_response.group_info.group_id
305+
306+
# Cancel reservation
307+
cancel_response = message_service.cancel_scheduled_message(group_id)
308+
309+
# Verify cancellation response
310+
assert cancel_response.group_id is not None
311+
312+
# Print cancellation information for verification
313+
# print(f"Cancellation status: {cancel_response.status}")
314+
print(f"Cancelled message ID: {cancel_response.group_id}")

0 commit comments

Comments
 (0)