blueticks 3.2.0__tar.gz → 3.3.0__tar.gz
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.
- {blueticks-3.2.0/src/blueticks.egg-info → blueticks-3.3.0}/PKG-INFO +1 -1
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/_client.py +2 -0
- blueticks-3.3.0/src/blueticks/_version.py +1 -0
- blueticks-3.3.0/src/blueticks/resources/__init__.py +27 -0
- blueticks-3.3.0/src/blueticks/resources/scheduled_messages.py +70 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/__init__.py +5 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/chats.py +19 -1
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/groups.py +11 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/messages.py +1 -1
- blueticks-3.3.0/src/blueticks/types/scheduled_messages.py +26 -0
- {blueticks-3.2.0 → blueticks-3.3.0/src/blueticks.egg-info}/PKG-INFO +1 -1
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks.egg-info/SOURCES.txt +3 -0
- blueticks-3.3.0/tests/test_scheduled_messages.py +155 -0
- blueticks-3.2.0/src/blueticks/_version.py +0 -1
- blueticks-3.2.0/src/blueticks/resources/__init__.py +0 -4
- {blueticks-3.2.0 → blueticks-3.3.0}/LICENSE +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/README.md +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/pyproject.toml +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/setup.cfg +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/__init__.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/_base_resource.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/_errors.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/_transport.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/py.typed +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/account.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/audiences.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/campaigns.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/chats.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/contacts.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/engines.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/groups.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/messages.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/ping.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/utils.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/resources/webhooks.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/account.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/audiences.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/campaigns.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/contacts.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/engines.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/page.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/ping.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/utils.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/types/webhooks.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks/webhooks.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks.egg-info/dependency_links.txt +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks.egg-info/requires.txt +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/src/blueticks.egg-info/top_level.txt +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_account.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_audiences.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_campaigns.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_client.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_errors.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_messages.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_ping.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_transport.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_types.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_webhook_verify.py +0 -0
- {blueticks-3.2.0 → blueticks-3.3.0}/tests/test_webhooks_resource.py +0 -0
|
@@ -69,6 +69,7 @@ class Blueticks:
|
|
|
69
69
|
from blueticks.resources.engines import EnginesResource
|
|
70
70
|
from blueticks.resources.groups import GroupsResource
|
|
71
71
|
from blueticks.resources.messages import MessagesResource
|
|
72
|
+
from blueticks.resources.scheduled_messages import ScheduledMessagesResource
|
|
72
73
|
from blueticks.resources.utils import UtilsResource
|
|
73
74
|
from blueticks.resources.webhooks import WebhooksResource
|
|
74
75
|
|
|
@@ -80,6 +81,7 @@ class Blueticks:
|
|
|
80
81
|
self.engines = EnginesResource(self)
|
|
81
82
|
self.groups = GroupsResource(self)
|
|
82
83
|
self.messages = MessagesResource(self)
|
|
84
|
+
self.scheduled_messages = ScheduledMessagesResource(self)
|
|
83
85
|
self.utils = UtilsResource(self)
|
|
84
86
|
self.webhooks = WebhooksResource(self)
|
|
85
87
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.3.0"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from blueticks.resources.account import AccountResource
|
|
2
|
+
from blueticks.resources.audiences import AudiencesResource
|
|
3
|
+
from blueticks.resources.campaigns import CampaignsResource
|
|
4
|
+
from blueticks.resources.chats import ChatsResource
|
|
5
|
+
from blueticks.resources.contacts import ContactsResource
|
|
6
|
+
from blueticks.resources.engines import EnginesResource
|
|
7
|
+
from blueticks.resources.groups import GroupsResource
|
|
8
|
+
from blueticks.resources.messages import MessagesResource
|
|
9
|
+
from blueticks.resources.ping import PingResource
|
|
10
|
+
from blueticks.resources.scheduled_messages import ScheduledMessagesResource
|
|
11
|
+
from blueticks.resources.utils import UtilsResource
|
|
12
|
+
from blueticks.resources.webhooks import WebhooksResource
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AccountResource",
|
|
16
|
+
"AudiencesResource",
|
|
17
|
+
"CampaignsResource",
|
|
18
|
+
"ChatsResource",
|
|
19
|
+
"ContactsResource",
|
|
20
|
+
"EnginesResource",
|
|
21
|
+
"GroupsResource",
|
|
22
|
+
"MessagesResource",
|
|
23
|
+
"PingResource",
|
|
24
|
+
"ScheduledMessagesResource",
|
|
25
|
+
"UtilsResource",
|
|
26
|
+
"WebhooksResource",
|
|
27
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from blueticks._base_resource import BaseResource
|
|
6
|
+
from blueticks.types.page import Page
|
|
7
|
+
from blueticks.types.scheduled_messages import ScheduledMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ScheduledMessagesResource(BaseResource):
|
|
11
|
+
def list(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
limit: int | None = None,
|
|
15
|
+
cursor: str | None = None,
|
|
16
|
+
) -> Page[ScheduledMessage]:
|
|
17
|
+
"""List scheduled messages.
|
|
18
|
+
|
|
19
|
+
Retrieves a list of all resources from the service.
|
|
20
|
+
"""
|
|
21
|
+
params: dict[str, Any] = {}
|
|
22
|
+
if limit is not None:
|
|
23
|
+
params["limit"] = limit
|
|
24
|
+
if cursor is not None:
|
|
25
|
+
params["cursor"] = cursor
|
|
26
|
+
data = self._client._request("GET", "/v1/scheduled-messages", params=params or None)
|
|
27
|
+
return Page[ScheduledMessage].model_validate(data)
|
|
28
|
+
|
|
29
|
+
def retrieve(self, scheduled_message_id: str) -> ScheduledMessage:
|
|
30
|
+
"""Get scheduled message.
|
|
31
|
+
|
|
32
|
+
Retrieves a single resource with the given id from the service.
|
|
33
|
+
"""
|
|
34
|
+
data = self._client._request("GET", f"/v1/scheduled-messages/{scheduled_message_id}")
|
|
35
|
+
return ScheduledMessage.model_validate(data)
|
|
36
|
+
|
|
37
|
+
def update(
|
|
38
|
+
self,
|
|
39
|
+
scheduled_message_id: str,
|
|
40
|
+
*,
|
|
41
|
+
text: str | None = None,
|
|
42
|
+
media_url: str | None = None,
|
|
43
|
+
media_caption: str | None = None,
|
|
44
|
+
send_at: str | None = None,
|
|
45
|
+
) -> ScheduledMessage:
|
|
46
|
+
"""Update scheduled message.
|
|
47
|
+
|
|
48
|
+
Updates the resource identified by id using data.
|
|
49
|
+
"""
|
|
50
|
+
body: dict[str, Any] = {}
|
|
51
|
+
if text is not None:
|
|
52
|
+
body["text"] = text
|
|
53
|
+
if media_url is not None:
|
|
54
|
+
body["media_url"] = media_url
|
|
55
|
+
if media_caption is not None:
|
|
56
|
+
body["media_caption"] = media_caption
|
|
57
|
+
if send_at is not None:
|
|
58
|
+
body["send_at"] = send_at
|
|
59
|
+
data = self._client._request(
|
|
60
|
+
"PATCH", f"/v1/scheduled-messages/{scheduled_message_id}", body=body
|
|
61
|
+
)
|
|
62
|
+
return ScheduledMessage.model_validate(data)
|
|
63
|
+
|
|
64
|
+
def delete(self, scheduled_message_id: str) -> None:
|
|
65
|
+
"""Cancel scheduled message.
|
|
66
|
+
|
|
67
|
+
Removes the resource with id.
|
|
68
|
+
"""
|
|
69
|
+
self._client._request("DELETE", f"/v1/scheduled-messages/{scheduled_message_id}")
|
|
70
|
+
return None
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from blueticks.types.account import Account
|
|
2
2
|
from blueticks.types.audiences import AppendContactsResult, Audience, Contact
|
|
3
3
|
from blueticks.types.campaigns import Campaign
|
|
4
|
+
from blueticks.types.groups import Group, GroupParticipant
|
|
4
5
|
from blueticks.types.messages import Message
|
|
5
6
|
from blueticks.types.page import Page
|
|
6
7
|
from blueticks.types.ping import Ping
|
|
8
|
+
from blueticks.types.scheduled_messages import ScheduledMessage
|
|
7
9
|
from blueticks.types.webhooks import Webhook, WebhookCreateResult, WebhookEvent
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
@@ -12,9 +14,12 @@ __all__ = [
|
|
|
12
14
|
"Audience",
|
|
13
15
|
"Campaign",
|
|
14
16
|
"Contact",
|
|
17
|
+
"Group",
|
|
18
|
+
"GroupParticipant",
|
|
15
19
|
"Message",
|
|
16
20
|
"Page",
|
|
17
21
|
"Ping",
|
|
22
|
+
"ScheduledMessage",
|
|
18
23
|
"Webhook",
|
|
19
24
|
"WebhookCreateResult",
|
|
20
25
|
"WebhookEvent",
|
|
@@ -70,6 +70,7 @@ class ChatMessage(BaseModel):
|
|
|
70
70
|
MediaUnavailableReason = Literal[
|
|
71
71
|
"expired",
|
|
72
72
|
"fetching",
|
|
73
|
+
"awaiting_sender",
|
|
73
74
|
"error",
|
|
74
75
|
"no_media",
|
|
75
76
|
]
|
|
@@ -87,4 +88,21 @@ class ChatMedia(BaseModel):
|
|
|
87
88
|
# genuine original from the sender.
|
|
88
89
|
original_quality: Optional[bool] = None # noqa: UP045
|
|
89
90
|
# Reason the bytes couldn't be retrieved. None/absent on success.
|
|
90
|
-
|
|
91
|
+
# "expired" = WA aged the file out of CDN retention. "fetching" = WA is
|
|
92
|
+
# mid-download from its CDN — transient, retry in a few seconds.
|
|
93
|
+
# "awaiting_sender" = WA's CDN doesn't have the file but has asked the
|
|
94
|
+
# sender's device to reupload it; only completes when the sender's
|
|
95
|
+
# WhatsApp client is open and online. "error" = unexpected failure.
|
|
96
|
+
# "no_media" = the message is text-only/revoked/location/vcard.
|
|
97
|
+
media_unavailable: Optional[MediaUnavailableReason] = Field( # noqa: UP045
|
|
98
|
+
default=None,
|
|
99
|
+
description=(
|
|
100
|
+
"Set when the bytes could not be downloaded or never existed. "
|
|
101
|
+
'"expired" = WA aged the file out of CDN retention. '
|
|
102
|
+
'"fetching" = WA is mid-download from its CDN — transient, retry in a few seconds. '
|
|
103
|
+
"\"awaiting_sender\" = WA's CDN does not have the file but has asked the sender's "
|
|
104
|
+
"device to reupload it; only completes when the sender's WhatsApp client is open and "
|
|
105
|
+
'online. "error" = unexpected failure. "no_media" = the message is text-only / revoked '
|
|
106
|
+
"/ location / vcard."
|
|
107
|
+
),
|
|
108
|
+
)
|
|
@@ -6,6 +6,15 @@ from typing import Optional # noqa: UP045
|
|
|
6
6
|
from pydantic import BaseModel, ConfigDict
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
class GroupParticipant(BaseModel):
|
|
10
|
+
model_config = ConfigDict(extra="ignore")
|
|
11
|
+
|
|
12
|
+
chat_id: str
|
|
13
|
+
is_admin: bool
|
|
14
|
+
is_super_admin: bool
|
|
15
|
+
name: Optional[str] = None # noqa: UP045
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
class Group(BaseModel):
|
|
10
19
|
model_config = ConfigDict(extra="ignore")
|
|
11
20
|
|
|
@@ -14,6 +23,8 @@ class Group(BaseModel):
|
|
|
14
23
|
description: Optional[str] = None # noqa: UP045
|
|
15
24
|
owner: Optional[str] = None # noqa: UP045
|
|
16
25
|
created_at: Optional[str] = None # noqa: UP045
|
|
26
|
+
last_message_at: Optional[str] = None # noqa: UP045
|
|
17
27
|
participant_count: Optional[int] = None # noqa: UP045
|
|
18
28
|
announce: Optional[bool] = None # noqa: UP045
|
|
19
29
|
restrict: Optional[bool] = None # noqa: UP045
|
|
30
|
+
participants: Optional[list[GroupParticipant]] = None # noqa: UP045
|
|
@@ -5,7 +5,7 @@ from typing import Literal, Optional
|
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, Field
|
|
7
7
|
|
|
8
|
-
MessageStatus = Literal["scheduled", "queued", "sending", "delivered", "failed"]
|
|
8
|
+
MessageStatus = Literal["scheduled", "queued", "sending", "delivered", "read", "failed"]
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Message(BaseModel):
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# ruff: noqa: UP045 # Pydantic field annotations need Optional[T] for Python 3.9 (see CLAUDE.md)
|
|
4
|
+
from typing import Optional # noqa: UP045
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ScheduledMessage(BaseModel):
|
|
10
|
+
"""A message queued for future delivery via /v1/scheduled-messages."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="ignore")
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
to: Optional[str] = None # noqa: UP045
|
|
16
|
+
text: Optional[str] = None # noqa: UP045
|
|
17
|
+
media_url: Optional[str] = None # noqa: UP045
|
|
18
|
+
media_caption: Optional[str] = None # noqa: UP045
|
|
19
|
+
media_filename: Optional[str] = None # noqa: UP045
|
|
20
|
+
media_mime_type: Optional[str] = None # noqa: UP045
|
|
21
|
+
send_at: Optional[str] = None # noqa: UP045
|
|
22
|
+
status: Optional[str] = None # noqa: UP045
|
|
23
|
+
is_recurring: bool
|
|
24
|
+
recurrence_rule: Optional[str] = None # noqa: UP045
|
|
25
|
+
created_at: Optional[str] = None # noqa: UP045
|
|
26
|
+
updated_at: Optional[str] = None # noqa: UP045
|
|
@@ -24,6 +24,7 @@ src/blueticks/resources/engines.py
|
|
|
24
24
|
src/blueticks/resources/groups.py
|
|
25
25
|
src/blueticks/resources/messages.py
|
|
26
26
|
src/blueticks/resources/ping.py
|
|
27
|
+
src/blueticks/resources/scheduled_messages.py
|
|
27
28
|
src/blueticks/resources/utils.py
|
|
28
29
|
src/blueticks/resources/webhooks.py
|
|
29
30
|
src/blueticks/types/__init__.py
|
|
@@ -37,6 +38,7 @@ src/blueticks/types/groups.py
|
|
|
37
38
|
src/blueticks/types/messages.py
|
|
38
39
|
src/blueticks/types/page.py
|
|
39
40
|
src/blueticks/types/ping.py
|
|
41
|
+
src/blueticks/types/scheduled_messages.py
|
|
40
42
|
src/blueticks/types/utils.py
|
|
41
43
|
src/blueticks/types/webhooks.py
|
|
42
44
|
tests/test_account.py
|
|
@@ -46,6 +48,7 @@ tests/test_client.py
|
|
|
46
48
|
tests/test_errors.py
|
|
47
49
|
tests/test_messages.py
|
|
48
50
|
tests/test_ping.py
|
|
51
|
+
tests/test_scheduled_messages.py
|
|
49
52
|
tests/test_transport.py
|
|
50
53
|
tests/test_types.py
|
|
51
54
|
tests/test_webhook_verify.py
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from blueticks import AuthenticationError
|
|
10
|
+
from blueticks.types.page import Page
|
|
11
|
+
from blueticks.types.scheduled_messages import ScheduledMessage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _scheduled(sid: str = "sched_1", **overrides) -> dict:
|
|
15
|
+
data = {
|
|
16
|
+
"id": sid,
|
|
17
|
+
"to": "+12025550100",
|
|
18
|
+
"text": "hello future",
|
|
19
|
+
"media_url": None,
|
|
20
|
+
"media_caption": None,
|
|
21
|
+
"media_filename": None,
|
|
22
|
+
"media_mime_type": None,
|
|
23
|
+
"send_at": "2026-06-01T09:00:00Z",
|
|
24
|
+
"status": "pending",
|
|
25
|
+
"is_recurring": False,
|
|
26
|
+
"recurrence_rule": None,
|
|
27
|
+
"created_at": "2026-04-23T00:00:00Z",
|
|
28
|
+
"updated_at": "2026-04-23T00:00:00Z",
|
|
29
|
+
}
|
|
30
|
+
data.update(overrides)
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_list_scheduled_messages(mock_client) -> None:
|
|
35
|
+
def handler(req: httpx.Request) -> httpx.Response:
|
|
36
|
+
assert req.method == "GET"
|
|
37
|
+
assert req.url.path == "/v1/scheduled-messages"
|
|
38
|
+
return httpx.Response(
|
|
39
|
+
200,
|
|
40
|
+
content=json.dumps(
|
|
41
|
+
{
|
|
42
|
+
"data": [_scheduled("sched_1"), _scheduled("sched_2", is_recurring=True)],
|
|
43
|
+
"has_more": False,
|
|
44
|
+
"next_cursor": None,
|
|
45
|
+
}
|
|
46
|
+
).encode(),
|
|
47
|
+
headers={"content-type": "application/json"},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
with mock_client(handler) as client:
|
|
51
|
+
page = client.scheduled_messages.list()
|
|
52
|
+
assert isinstance(page, Page)
|
|
53
|
+
assert len(page.data) == 2
|
|
54
|
+
assert page.data[0].id == "sched_1"
|
|
55
|
+
assert page.data[1].is_recurring is True
|
|
56
|
+
assert page.has_more is False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_list_scheduled_messages_passes_pagination_params(mock_client) -> None:
|
|
60
|
+
seen_params: dict = {}
|
|
61
|
+
|
|
62
|
+
def handler(req: httpx.Request) -> httpx.Response:
|
|
63
|
+
seen_params.update(dict(req.url.params))
|
|
64
|
+
return httpx.Response(
|
|
65
|
+
200,
|
|
66
|
+
content=json.dumps({"data": [], "has_more": True, "next_cursor": "cur_next"}).encode(),
|
|
67
|
+
headers={"content-type": "application/json"},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
with mock_client(handler) as client:
|
|
71
|
+
page = client.scheduled_messages.list(limit=25, cursor="cur_abc")
|
|
72
|
+
assert seen_params == {"limit": "25", "cursor": "cur_abc"}
|
|
73
|
+
assert page.next_cursor == "cur_next"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_retrieve_scheduled_message(mock_client) -> None:
|
|
77
|
+
def handler(req: httpx.Request) -> httpx.Response:
|
|
78
|
+
assert req.method == "GET"
|
|
79
|
+
assert req.url.path == "/v1/scheduled-messages/sched_1"
|
|
80
|
+
return httpx.Response(
|
|
81
|
+
200,
|
|
82
|
+
content=json.dumps(_scheduled("sched_1", text="reminder")).encode(),
|
|
83
|
+
headers={"content-type": "application/json"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
with mock_client(handler) as client:
|
|
87
|
+
result = client.scheduled_messages.retrieve("sched_1")
|
|
88
|
+
assert isinstance(result, ScheduledMessage)
|
|
89
|
+
assert result.id == "sched_1"
|
|
90
|
+
assert result.text == "reminder"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_update_scheduled_message(mock_client) -> None:
|
|
94
|
+
body_seen: dict = {}
|
|
95
|
+
|
|
96
|
+
def handler(req: httpx.Request) -> httpx.Response:
|
|
97
|
+
body_seen.update(json.loads(req.content))
|
|
98
|
+
assert req.method == "PATCH"
|
|
99
|
+
assert req.url.path == "/v1/scheduled-messages/sched_1"
|
|
100
|
+
return httpx.Response(
|
|
101
|
+
200,
|
|
102
|
+
content=json.dumps(_scheduled("sched_1", text="updated")).encode(),
|
|
103
|
+
headers={"content-type": "application/json"},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with mock_client(handler) as client:
|
|
107
|
+
result = client.scheduled_messages.update(
|
|
108
|
+
"sched_1",
|
|
109
|
+
text="updated",
|
|
110
|
+
send_at="2026-07-01T09:00:00Z",
|
|
111
|
+
)
|
|
112
|
+
assert result.text == "updated"
|
|
113
|
+
assert body_seen == {"text": "updated", "send_at": "2026-07-01T09:00:00Z"}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_delete_scheduled_message_returns_none(mock_client) -> None:
|
|
117
|
+
def handler(req: httpx.Request) -> httpx.Response:
|
|
118
|
+
assert req.method == "DELETE"
|
|
119
|
+
assert req.url.path == "/v1/scheduled-messages/sched_1"
|
|
120
|
+
return httpx.Response(204)
|
|
121
|
+
|
|
122
|
+
with mock_client(handler) as client:
|
|
123
|
+
assert client.scheduled_messages.delete("sched_1") is None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_scheduled_messages_propagates_authentication_error(mock_client) -> None:
|
|
127
|
+
def handler(_req: httpx.Request) -> httpx.Response:
|
|
128
|
+
body = {
|
|
129
|
+
"error": {
|
|
130
|
+
"code": "authentication_required",
|
|
131
|
+
"message": "bad key",
|
|
132
|
+
"request_id": "req_z",
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return httpx.Response(
|
|
136
|
+
401,
|
|
137
|
+
content=json.dumps(body).encode(),
|
|
138
|
+
headers={"content-type": "application/json"},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with mock_client(handler) as client:
|
|
142
|
+
with pytest.raises(AuthenticationError) as info:
|
|
143
|
+
client.scheduled_messages.retrieve("sched_1")
|
|
144
|
+
assert info.value.code == "authentication_required"
|
|
145
|
+
assert info.value.message == "bad key"
|
|
146
|
+
assert info.value.request_id == "req_z"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_scheduled_messages_missing_required_field_raises_validation_error(mock_client) -> None:
|
|
150
|
+
def handler(_req: httpx.Request) -> httpx.Response:
|
|
151
|
+
return httpx.Response(200, content=b"{}", headers={"content-type": "application/json"})
|
|
152
|
+
|
|
153
|
+
with mock_client(handler) as client:
|
|
154
|
+
with pytest.raises(ValidationError):
|
|
155
|
+
client.scheduled_messages.retrieve("sched_1")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|