Skip to content

Commit ef5fbde

Browse files
committed
Merge remote-tracking branch 'origin/master' into claude/add-github-actions-ci-75v3Y
# Conflicts: # python_anticaptcha/base.py
2 parents 6e6f4a1 + 20252da commit ef5fbde

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

python_anticaptcha/base.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def join(
8282
self,
8383
maximum_time: int | None = None,
8484
on_check: Callable[[int, str | None], None] | None = None,
85+
backoff: bool = False,
8586
) -> None:
8687
"""Poll for task completion, blocking until ready or timeout.
8788
@@ -90,13 +91,19 @@ def join(
9091
``(elapsed_time, status)`` where *elapsed_time* is the total seconds
9192
waited so far and *status* is the last task status string
9293
(e.g. ``"processing"``).
94+
:param backoff: When ``True``, use exponential backoff for polling
95+
intervals starting at 1 second and doubling up to a 10-second cap.
96+
Default ``False`` preserves the fixed 3-second interval.
9397
:raises AnticaptchaException: If *maximum_time* is exceeded.
9498
"""
9599
elapsed_time = 0
96100
maximum_time = maximum_time or MAXIMUM_JOIN_TIME
101+
sleep_time = 1 if backoff else SLEEP_EVERY_CHECK_FINISHED
97102
while not self.check_is_ready():
98-
time.sleep(SLEEP_EVERY_CHECK_FINISHED)
99-
elapsed_time += SLEEP_EVERY_CHECK_FINISHED
103+
time.sleep(sleep_time)
104+
elapsed_time += sleep_time
105+
if backoff:
106+
sleep_time = min(sleep_time * 2, 10)
100107
if on_check is not None and self._last_result is not None:
101108
on_check(elapsed_time, self._last_result.get("status"))
102109
if elapsed_time > maximum_time:

tests/test_base.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,67 @@ def test_on_check_not_called_when_immediately_ready(self, mock_sleep):
200200
callback.assert_not_called()
201201

202202

203+
class TestJobJoinBackoff:
204+
@patch("python_anticaptcha.base.time.sleep")
205+
def test_backoff_sleep_schedule(self, mock_sleep):
206+
client = MagicMock()
207+
# processing 6 times then ready — enough to hit the 10s cap
208+
client.getTaskResult.side_effect = [
209+
{"status": "processing"},
210+
{"status": "processing"},
211+
{"status": "processing"},
212+
{"status": "processing"},
213+
{"status": "processing"},
214+
{"status": "processing"},
215+
{"status": "ready", "solution": {}},
216+
]
217+
job = Job(client, task_id=1)
218+
job.join(backoff=True)
219+
sleep_values = [call.args[0] for call in mock_sleep.call_args_list]
220+
assert sleep_values == [1, 2, 4, 8, 10, 10]
221+
222+
@patch("python_anticaptcha.base.time.sleep")
223+
def test_backoff_false_uses_fixed_interval(self, mock_sleep):
224+
client = MagicMock()
225+
client.getTaskResult.side_effect = [
226+
{"status": "processing"},
227+
{"status": "processing"},
228+
{"status": "processing"},
229+
{"status": "ready", "solution": {}},
230+
]
231+
job = Job(client, task_id=1)
232+
job.join(backoff=False)
233+
sleep_values = [call.args[0] for call in mock_sleep.call_args_list]
234+
assert sleep_values == [SLEEP_EVERY_CHECK_FINISHED] * 3
235+
236+
@patch("python_anticaptcha.base.time.sleep")
237+
def test_backoff_timeout_still_works(self, mock_sleep):
238+
client = MagicMock()
239+
client.getTaskResult.return_value = {"status": "processing"}
240+
job = Job(client, task_id=1)
241+
with pytest.raises(AnticaptchaException) as exc_info:
242+
job.join(maximum_time=5, backoff=True)
243+
assert "exceeded" in str(exc_info.value).lower()
244+
245+
@patch("python_anticaptcha.base.time.sleep")
246+
def test_backoff_with_on_check(self, mock_sleep):
247+
client = MagicMock()
248+
client.getTaskResult.side_effect = [
249+
{"status": "processing"},
250+
{"status": "processing"},
251+
{"status": "processing"},
252+
{"status": "ready", "solution": {}},
253+
]
254+
job = Job(client, task_id=1)
255+
callback = MagicMock()
256+
job.join(backoff=True, on_check=callback)
257+
assert callback.call_count == 3
258+
# Elapsed times: 1, 1+2=3, 3+4=7
259+
callback.assert_any_call(1, "processing")
260+
callback.assert_any_call(3, "processing")
261+
callback.assert_any_call(7, "processing")
262+
263+
203264
class TestContextManager:
204265
def test_enter_returns_self(self):
205266
client = AnticaptchaClient("key123")

0 commit comments

Comments
 (0)