Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- Validate `interval_seconds` and `timeout_seconds` in `calls.wait_for_result` and
`calls.create_and_wait` the same way the Goal Run helpers do. Zero, negative,
NaN and infinite values now raise a `ValueError` naming the argument, and
`create_and_wait` raises before the create request is sent.

## [1.0.1] - 2026-09-23

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions src/calle/calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import httpx

from calle.errors import CalleConnectionError, CalleTimeoutError, api_error_from_response
from calle.goals import _validate_polling_seconds


JsonObject = dict[str, Any]
Expand Down Expand Up @@ -54,6 +55,8 @@ def wait_for_result(
interval_seconds: float = 2.0,
timeout_seconds: float = 600.0,
) -> JsonObject:
_validate_polling_seconds(interval_seconds, "interval_seconds")
_validate_polling_seconds(timeout_seconds, "timeout_seconds")
deadline = time.monotonic() + timeout_seconds
while time.monotonic() <= deadline:
call = self.get(call_id)
Expand All @@ -65,6 +68,8 @@ def wait_for_result(
def create_and_wait(self, **kwargs: Any) -> JsonObject:
interval_seconds = float(kwargs.pop("interval_seconds", 2.0))
timeout_seconds = float(kwargs.pop("timeout_seconds", 600.0))
_validate_polling_seconds(interval_seconds, "interval_seconds")
_validate_polling_seconds(timeout_seconds, "timeout_seconds")
call = self.create(**kwargs)
return self.wait_for_result(
str(call["id"]),
Expand Down
56 changes: 56 additions & 0 deletions tests/test_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,59 @@ def handle_request(request: httpx.Request) -> httpx.Response:
encoded_id = "%2E%2E%2Fgoals%3Fadmin%3D1%23fragment%252F"
assert requests[0].url.raw_path == f"/v2/calls/{encoded_id}".encode()
assert requests[1].url.raw_path == f"/v2/calls/{encoded_id}/events".encode()


INVALID_POLLING_SECONDS = [
("interval_seconds", 0.0),
("interval_seconds", -1.0),
("interval_seconds", float("nan")),
("interval_seconds", float("inf")),
("timeout_seconds", 0.0),
("timeout_seconds", -1.0),
("timeout_seconds", float("nan")),
("timeout_seconds", float("inf")),
]


@pytest.mark.parametrize(("parameter", "value"), INVALID_POLLING_SECONDS)
@respx.mock
def test_wait_for_result_rejects_invalid_polling_seconds_without_request(
parameter: str,
value: float,
) -> None:
route = respx.get("https://api.heycall-e.com/v2/calls/call_123").mock(
return_value=httpx.Response(200, json=COMPLETED_CALL)
)
client = CalleClient(api_key="key_test")
options = {"interval_seconds": 1.0, "timeout_seconds": 1.0}
options[parameter] = value

with pytest.raises(ValueError, match=f"{parameter} must be a finite positive number"):
client.calls.wait_for_result("call_123", **options)

assert route.call_count == 0


@pytest.mark.parametrize(("parameter", "value"), INVALID_POLLING_SECONDS)
@respx.mock
def test_create_and_wait_rejects_invalid_polling_seconds_before_create(
parameter: str,
value: float,
) -> None:
route = respx.post("https://api.heycall-e.com/v2/calls").mock(
return_value=httpx.Response(201, json=COMPLETED_CALL)
)
client = CalleClient(api_key="key_test")
options = {"interval_seconds": 1.0, "timeout_seconds": 1.0}
options[parameter] = value

with pytest.raises(ValueError, match=f"{parameter} must be a finite positive number"):
client.calls.create_and_wait(
task="Call.",
phone="+14155550100",
idempotency_key="call:test:v1",
result_schema={"type": "object", "properties": {}, "additionalProperties": False},
**options,
)

assert route.call_count == 0