Skip to content
Merged
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
12 changes: 9 additions & 3 deletions python_anticaptcha/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,27 @@ def __repr__(self) -> str:
return f"<Job task_id={self.task_id} status={status!r}>"
return f"<Job task_id={self.task_id}>"

def join(self, maximum_time: int | None = None, on_check=None) -> None:
def join(self, maximum_time: int | None = None, on_check=None, backoff: bool = False) -> None:
"""Poll for task completion, blocking until ready or timeout.

:param maximum_time: Maximum seconds to wait (default: ``MAXIMUM_JOIN_TIME``).
:param on_check: Optional callback invoked after each poll with
``(elapsed_time, status)`` where *elapsed_time* is the total seconds
waited so far and *status* is the last task status string
(e.g. ``"processing"``).
:param backoff: When ``True``, use exponential backoff for polling
intervals starting at 1 second and doubling up to a 10-second cap.
Default ``False`` preserves the fixed 3-second interval.
:raises AnticaptchaException: If *maximum_time* is exceeded.
"""
elapsed_time = 0
maximum_time = maximum_time or MAXIMUM_JOIN_TIME
sleep_time = 1 if backoff else SLEEP_EVERY_CHECK_FINISHED
while not self.check_is_ready():
time.sleep(SLEEP_EVERY_CHECK_FINISHED)
elapsed_time += SLEEP_EVERY_CHECK_FINISHED
time.sleep(sleep_time)
elapsed_time += sleep_time
if backoff:
sleep_time = min(sleep_time * 2, 10)
if on_check is not None:
on_check(elapsed_time, self._last_result.get("status"))
if elapsed_time > maximum_time:
Expand Down
61 changes: 61 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,67 @@ def test_on_check_not_called_when_immediately_ready(self, mock_sleep):
callback.assert_not_called()


class TestJobJoinBackoff:
@patch("python_anticaptcha.base.time.sleep")
def test_backoff_sleep_schedule(self, mock_sleep):
client = MagicMock()
# processing 6 times then ready — enough to hit the 10s cap
client.getTaskResult.side_effect = [
{"status": "processing"},
{"status": "processing"},
{"status": "processing"},
{"status": "processing"},
{"status": "processing"},
{"status": "processing"},
{"status": "ready", "solution": {}},
]
job = Job(client, task_id=1)
job.join(backoff=True)
sleep_values = [call.args[0] for call in mock_sleep.call_args_list]
assert sleep_values == [1, 2, 4, 8, 10, 10]

@patch("python_anticaptcha.base.time.sleep")
def test_backoff_false_uses_fixed_interval(self, mock_sleep):
client = MagicMock()
client.getTaskResult.side_effect = [
{"status": "processing"},
{"status": "processing"},
{"status": "processing"},
{"status": "ready", "solution": {}},
]
job = Job(client, task_id=1)
job.join(backoff=False)
sleep_values = [call.args[0] for call in mock_sleep.call_args_list]
assert sleep_values == [SLEEP_EVERY_CHECK_FINISHED] * 3

@patch("python_anticaptcha.base.time.sleep")
def test_backoff_timeout_still_works(self, mock_sleep):
client = MagicMock()
client.getTaskResult.return_value = {"status": "processing"}
job = Job(client, task_id=1)
with pytest.raises(AnticaptchaException) as exc_info:
job.join(maximum_time=5, backoff=True)
assert "exceeded" in str(exc_info.value).lower()

@patch("python_anticaptcha.base.time.sleep")
def test_backoff_with_on_check(self, mock_sleep):
client = MagicMock()
client.getTaskResult.side_effect = [
{"status": "processing"},
{"status": "processing"},
{"status": "processing"},
{"status": "ready", "solution": {}},
]
job = Job(client, task_id=1)
callback = MagicMock()
job.join(backoff=True, on_check=callback)
assert callback.call_count == 3
# Elapsed times: 1, 1+2=3, 3+4=7
callback.assert_any_call(1, "processing")
callback.assert_any_call(3, "processing")
callback.assert_any_call(7, "processing")


class TestContextManager:
def test_enter_returns_self(self):
client = AnticaptchaClient("key123")
Expand Down
Loading