Skip to content

Commit 39751e5

Browse files
mikemolinetclaude
andauthored
feat(messages): add send_at per-message scheduling (cueapi #623 parity) (#34)
Adds the optional ``send_at`` kwarg to ``MessagesResource.send()``, covering server PR #623 (POST /v1/messages send_at). Caller can now schedule per-message delivery instead of always-immediate. Accepts ``str`` (ISO 8601) or ``datetime`` — auto-serialized via ``.isoformat()`` (same convention as ``cues.create(at=...)`` and the new ``cues.fire(send_at=...)`` from PR #33). Body field, not header (matches the server contract). Backwards compatible — defaults to None, omitted from the request body when unset; existing call sites unchanged. 3 new mock-based tests in test_messages_resource.py::TestSendAt (matches existing test style in this file: assert on the request body shape rather than against staging): - send_at as ISO 8601 string flows verbatim - send_at as datetime auto-isoformats with tz - send_at unset omits field from body entirely Source: drift audit handoff/cueapi-package-drift-2026-05-06; Backlog row "Parity port: PR #623 (POST /v1/messages send_at) → cueapi-python" (p1, CTO-SEC-DRIFT-AUDIT-AUTHORIZE 2026-05-06). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ada8c4d commit 39751e5

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

cueapi/resources/messages.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Any, Dict, Optional
5+
from datetime import datetime
6+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
67

78
if TYPE_CHECKING:
89
from cueapi.client import CueAPI
@@ -33,6 +34,7 @@ def send(
3334
reply_to_agent: Optional[str] = None,
3435
metadata: Optional[Dict[str, Any]] = None,
3536
idempotency_key: Optional[str] = None,
37+
send_at: Optional[Union[str, datetime]] = None,
3638
) -> dict:
3739
"""Send a message.
3840
@@ -69,6 +71,11 @@ def send(
6971
metadata: Optional JSON metadata blob.
7072
idempotency_key: Optional ``Idempotency-Key`` header
7173
(≤255 chars).
74+
send_at: Optional ISO 8601 timestamp (or ``datetime``) to
75+
delay this message's delivery. If omitted, the message
76+
is delivered immediately. Per-message scheduling landed
77+
in cueapi #623 — server stores ``send_at`` on the
78+
message row and the worker picks it up when due.
7279
7380
Returns:
7481
Dict matching the server's ``MessageResponse`` shape.
@@ -95,6 +102,10 @@ def send(
95102
payload["reply_to_agent"] = reply_to_agent
96103
if metadata is not None:
97104
payload["metadata"] = metadata
105+
if send_at is not None:
106+
payload["send_at"] = (
107+
send_at.isoformat() if isinstance(send_at, datetime) else send_at
108+
)
98109

99110
headers: Dict[str, str] = {"X-Cueapi-From-Agent": from_agent}
100111
if idempotency_key is not None:

tests/test_messages_resource.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,64 @@ def test_ack(self):
137137
mock_client._post.assert_called_once_with(
138138
"/v1/messages/msg_x/ack", json={},
139139
)
140+
141+
142+
class TestSendAt:
143+
"""Per-message scheduling via send_at (cueapi #623 parity port).
144+
145+
Mock-based following the existing pattern in this file. Asserts on
146+
the request body shape — that's the SDK contract; what the server
147+
does with it (delay then deliver) is exercised by the server suite.
148+
"""
149+
150+
def test_send_with_send_at_iso_string(self):
151+
"""send_at as ISO 8601 string flows into the request body verbatim."""
152+
mock_client = MagicMock()
153+
mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"}
154+
r = MessagesResource(mock_client)
155+
156+
r.send(
157+
from_agent="sender@x",
158+
to="recipient@y",
159+
body="hi",
160+
send_at="2030-01-01T12:00:00Z",
161+
)
162+
163+
mock_client._post.assert_called_once_with(
164+
"/v1/messages",
165+
json={
166+
"to": "recipient@y",
167+
"body": "hi",
168+
"send_at": "2030-01-01T12:00:00Z",
169+
},
170+
headers={"X-Cueapi-From-Agent": "sender@x"},
171+
)
172+
173+
def test_send_with_send_at_datetime_auto_isoformats(self):
174+
"""send_at as datetime auto-serializes via .isoformat()."""
175+
from datetime import datetime, timezone
176+
177+
mock_client = MagicMock()
178+
mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"}
179+
r = MessagesResource(mock_client)
180+
181+
r.send(
182+
from_agent="sender@x",
183+
to="recipient@y",
184+
body="hi",
185+
send_at=datetime(2030, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
186+
)
187+
188+
call = mock_client._post.call_args
189+
assert call.kwargs["json"]["send_at"] == "2030-01-01T12:00:00+00:00"
190+
191+
def test_send_without_send_at_omits_field(self):
192+
"""send_at unset → field NOT present in request body."""
193+
mock_client = MagicMock()
194+
mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"}
195+
r = MessagesResource(mock_client)
196+
197+
r.send(from_agent="sender@x", to="recipient@y", body="hi")
198+
199+
call = mock_client._post.call_args
200+
assert "send_at" not in call.kwargs["json"]

0 commit comments

Comments
 (0)