Skip to content

Commit 11b67b4

Browse files
ad-mAdam Dobrawyclaude
authored
Add optional on_check callback to Job.join() (#125)
* Add optional on_check callback to Job.join() Allow users to monitor progress during the blocking join() call by passing an on_check callback that receives (elapsed_time, status) after each polling iteration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add docstring to Job.join() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Adam Dobrawy <naczelnik@jawne.info.pl> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5a20a34 commit 11b67b4

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

python_anticaptcha/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,23 @@ def __repr__(self) -> str:
6969
return f"<Job task_id={self.task_id} status={status!r}>"
7070
return f"<Job task_id={self.task_id}>"
7171

72-
def join(self, maximum_time: int | None = None) -> None:
72+
def join(self, maximum_time: int | None = None, on_check=None) -> None:
73+
"""Poll for task completion, blocking until ready or timeout.
74+
75+
:param maximum_time: Maximum seconds to wait (default: ``MAXIMUM_JOIN_TIME``).
76+
:param on_check: Optional callback invoked after each poll with
77+
``(elapsed_time, status)`` where *elapsed_time* is the total seconds
78+
waited so far and *status* is the last task status string
79+
(e.g. ``"processing"``).
80+
:raises AnticaptchaException: If *maximum_time* is exceeded.
81+
"""
7382
elapsed_time = 0
7483
maximum_time = maximum_time or MAXIMUM_JOIN_TIME
7584
while not self.check_is_ready():
7685
time.sleep(SLEEP_EVERY_CHECK_FINISHED)
7786
elapsed_time += SLEEP_EVERY_CHECK_FINISHED
87+
if on_check is not None:
88+
on_check(elapsed_time, self._last_result.get("status"))
7889
if elapsed_time > maximum_time:
7990
raise AnticaptchaException(
8091
None,

tests/test_base.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,42 @@ def test_timeout_raises(self, mock_sleep):
166166
assert "exceeded" in str(exc_info.value).lower()
167167

168168

169+
class TestJobJoinOnCheck:
170+
@patch("python_anticaptcha.base.time.sleep")
171+
def test_on_check_called_each_iteration(self, mock_sleep):
172+
client = MagicMock()
173+
# Return processing twice, then ready
174+
client.getTaskResult.side_effect = [
175+
{"status": "processing"},
176+
{"status": "processing"},
177+
{"status": "ready", "solution": {}},
178+
]
179+
job = Job(client, task_id=1)
180+
callback = MagicMock()
181+
job.join(on_check=callback)
182+
assert callback.call_count == 2
183+
# Verify elapsed time and status passed correctly
184+
callback.assert_any_call(SLEEP_EVERY_CHECK_FINISHED, "processing")
185+
callback.assert_any_call(SLEEP_EVERY_CHECK_FINISHED * 2, "processing")
186+
187+
@patch("python_anticaptcha.base.time.sleep")
188+
def test_on_check_none_by_default(self, mock_sleep):
189+
client = MagicMock()
190+
client.getTaskResult.return_value = {"status": "ready", "solution": {}}
191+
job = Job(client, task_id=1)
192+
# Should not raise when on_check is not provided
193+
job.join()
194+
195+
@patch("python_anticaptcha.base.time.sleep")
196+
def test_on_check_not_called_when_immediately_ready(self, mock_sleep):
197+
client = MagicMock()
198+
client.getTaskResult.return_value = {"status": "ready", "solution": {}}
199+
job = Job(client, task_id=1)
200+
callback = MagicMock()
201+
job.join(on_check=callback)
202+
callback.assert_not_called()
203+
204+
169205
class TestContextManager:
170206
def test_enter_returns_self(self):
171207
client = AnticaptchaClient("key123")

0 commit comments

Comments
 (0)