Skip to content

Commit 6ff4b51

Browse files
committed
feat(cues): client.cues.fire(send_at=...) (hosted PR #618 port)
Adds optional `send_at` kwarg to `client.cues.fire()` for per-fire scheduling. Server-side FireRequest gained `send_at: Optional[datetime]` in #618; the dispatcher gates on dispatch_outbox.scheduled_at. Past timestamps are forgiving (server treats as 'fire now', idempotent — no error for a few-ms-late caller). Tests: 3 new (4 → 7 in TestFire). Pinned: send_at omitted from body when default None; combines correctly with payload_override + merge_strategy. Depends on cueapi/cueapi#618 merging to staging then prod for user-visible behavior. PR can merge independently — sending an unknown send_at field against an old server is silently ignored (Pydantic extra='ignore' default on FireRequest). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent a2b5325 commit 6ff4b51

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

cueapi/resources/cues.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def fire(
225225
*,
226226
payload_override: Optional[Dict[str, Any]] = None,
227227
merge_strategy: Optional[str] = None,
228+
send_at: Optional[str] = None,
228229
) -> Dict[str, Any]:
229230
"""Fire an existing cue immediately, optionally overriding its payload.
230231
@@ -241,6 +242,14 @@ def fire(
241242
payload. "merge" (server default) shallow-merges with override
242243
wins on key collisions. "replace" uses override as the final
243244
payload, ignoring cue.payload.
245+
send_at: Optional ISO 8601 UTC timestamp to schedule this fire for
246+
the future (hosted PR #618). When set and in the future, the
247+
resulting execution sits in ``pending`` until ``send_at <=
248+
now()``, then enters the normal dispatch path. Past timestamps
249+
are treated as "fire now" — idempotent and forgiving (no error
250+
for a few-ms-late caller). Server's ``FireRequest.send_at`` is
251+
``Optional[datetime]``; passing an ISO string is fine
252+
server-side.
244253
245254
Returns:
246255
The execution dict (id, scheduled_for, status, etc.).
@@ -250,4 +259,6 @@ def fire(
250259
body["payload_override"] = payload_override
251260
if merge_strategy is not None:
252261
body["merge_strategy"] = merge_strategy
262+
if send_at is not None:
263+
body["send_at"] = send_at
253264
return self._client._post(f"/v1/cues/{cue_id}/fire", json=body)

tests/test_cues_resource.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,54 @@ def test_fire_omits_merge_strategy_when_not_passed(self):
5555

5656
sent_body = mock_client._post.call_args.kwargs["json"]
5757
assert "merge_strategy" not in sent_body
58+
59+
def test_fire_with_send_at(self):
60+
# Hosted PR #618: per-fire scheduling. send_at as ISO string lands
61+
# in the body unchanged.
62+
mock_client = MagicMock()
63+
mock_client._post.return_value = {
64+
"id": "exec_test",
65+
"scheduled_for": "2026-05-04T20:00:00Z",
66+
}
67+
resource = CuesResource(mock_client)
68+
69+
resource.fire("cue_abc123", send_at="2026-05-04T20:00:00Z")
70+
71+
mock_client._post.assert_called_once_with(
72+
"/v1/cues/cue_abc123/fire",
73+
json={"send_at": "2026-05-04T20:00:00Z"},
74+
)
75+
76+
def test_fire_omits_send_at_when_not_passed(self):
77+
# Pin: when send_at is None (default), the body must not include the
78+
# key. Sending null adds noise to the request and may interact poorly
79+
# with future Pydantic schemas.
80+
mock_client = MagicMock()
81+
mock_client._post.return_value = {"id": "exec_test"}
82+
resource = CuesResource(mock_client)
83+
84+
resource.fire("cue_abc123")
85+
86+
sent_body = mock_client._post.call_args.kwargs["json"]
87+
assert "send_at" not in sent_body
88+
89+
def test_fire_combines_send_at_with_payload_override(self):
90+
mock_client = MagicMock()
91+
mock_client._post.return_value = {"id": "exec_test"}
92+
resource = CuesResource(mock_client)
93+
94+
resource.fire(
95+
"cue_abc123",
96+
payload_override={"task": "demo"},
97+
merge_strategy="replace",
98+
send_at="2026-05-04T22:00:00Z",
99+
)
100+
101+
mock_client._post.assert_called_once_with(
102+
"/v1/cues/cue_abc123/fire",
103+
json={
104+
"payload_override": {"task": "demo"},
105+
"merge_strategy": "replace",
106+
"send_at": "2026-05-04T22:00:00Z",
107+
},
108+
)

0 commit comments

Comments
 (0)