diff --git a/cueapi/resources/cues.py b/cueapi/resources/cues.py index 5ad2c00..9b1c324 100644 --- a/cueapi/resources/cues.py +++ b/cueapi/resources/cues.py @@ -225,6 +225,7 @@ def fire( *, payload_override: Optional[Dict[str, Any]] = None, merge_strategy: Optional[str] = None, + send_at: Optional[str] = None, ) -> Dict[str, Any]: """Fire an existing cue immediately, optionally overriding its payload. @@ -241,6 +242,14 @@ def fire( payload. "merge" (server default) shallow-merges with override wins on key collisions. "replace" uses override as the final payload, ignoring cue.payload. + send_at: Optional ISO 8601 UTC timestamp to schedule this fire for + the future (hosted PR #618). When set and in the future, the + resulting execution sits in ``pending`` until ``send_at <= + now()``, then enters the normal dispatch path. Past timestamps + are treated as "fire now" — idempotent and forgiving (no error + for a few-ms-late caller). Server's ``FireRequest.send_at`` is + ``Optional[datetime]``; passing an ISO string is fine + server-side. Returns: The execution dict (id, scheduled_for, status, etc.). @@ -250,4 +259,6 @@ def fire( body["payload_override"] = payload_override if merge_strategy is not None: body["merge_strategy"] = merge_strategy + if send_at is not None: + body["send_at"] = send_at return self._client._post(f"/v1/cues/{cue_id}/fire", json=body) diff --git a/tests/test_cues_resource.py b/tests/test_cues_resource.py index fa7f6ec..42c0e99 100644 --- a/tests/test_cues_resource.py +++ b/tests/test_cues_resource.py @@ -55,3 +55,54 @@ def test_fire_omits_merge_strategy_when_not_passed(self): sent_body = mock_client._post.call_args.kwargs["json"] assert "merge_strategy" not in sent_body + + def test_fire_with_send_at(self): + # Hosted PR #618: per-fire scheduling. send_at as ISO string lands + # in the body unchanged. + mock_client = MagicMock() + mock_client._post.return_value = { + "id": "exec_test", + "scheduled_for": "2026-05-04T20:00:00Z", + } + resource = CuesResource(mock_client) + + resource.fire("cue_abc123", send_at="2026-05-04T20:00:00Z") + + mock_client._post.assert_called_once_with( + "/v1/cues/cue_abc123/fire", + json={"send_at": "2026-05-04T20:00:00Z"}, + ) + + def test_fire_omits_send_at_when_not_passed(self): + # Pin: when send_at is None (default), the body must not include the + # key. Sending null adds noise to the request and may interact poorly + # with future Pydantic schemas. + mock_client = MagicMock() + mock_client._post.return_value = {"id": "exec_test"} + resource = CuesResource(mock_client) + + resource.fire("cue_abc123") + + sent_body = mock_client._post.call_args.kwargs["json"] + assert "send_at" not in sent_body + + def test_fire_combines_send_at_with_payload_override(self): + mock_client = MagicMock() + mock_client._post.return_value = {"id": "exec_test"} + resource = CuesResource(mock_client) + + resource.fire( + "cue_abc123", + payload_override={"task": "demo"}, + merge_strategy="replace", + send_at="2026-05-04T22:00:00Z", + ) + + mock_client._post.assert_called_once_with( + "/v1/cues/cue_abc123/fire", + json={ + "payload_override": {"task": "demo"}, + "merge_strategy": "replace", + "send_at": "2026-05-04T22:00:00Z", + }, + )