ionq_core.polling

Job polling helpers for waiting on quantum job completion.

After submitting a job, use wait_for_job (or async_wait_for_job) to block until it reaches a terminal state (completed, failed, or canceled). Polling starts at _DEFAULT_INTERVAL and grows by _BACKOFF_FACTOR each iteration up to _MAX_INTERVAL; the default total wait is _DEFAULT_TIMEOUT seconds.

Example:
from ionq_core import IonQClient, wait_for_job
from ionq_core.api.default import create_job

client = IonQClient()
job = create_job.sync(client=client, body=payload)
completed = wait_for_job(client, job.id, timeout=300)
print(completed.status)  # "completed"
  1# SPDX-FileCopyrightText: 2026 IonQ, Inc.
  2# SPDX-License-Identifier: Apache-2.0
  3
  4"""Job polling helpers for waiting on quantum job completion.
  5
  6After submitting a job, use `wait_for_job` (or `async_wait_for_job`) to
  7block until it reaches a terminal state (completed, failed, or canceled).
  8Polling starts at `_DEFAULT_INTERVAL` and grows by `_BACKOFF_FACTOR` each
  9iteration up to `_MAX_INTERVAL`; the default total wait is
 10`_DEFAULT_TIMEOUT` seconds.
 11
 12Example:
 13    ```python
 14    from ionq_core import IonQClient, wait_for_job
 15    from ionq_core.api.default import create_job
 16
 17    client = IonQClient()
 18    job = create_job.sync(client=client, body=payload)
 19    completed = wait_for_job(client, job.id, timeout=300)
 20    print(completed.status)  # "completed"
 21    ```
 22"""
 23
 24from __future__ import annotations
 25
 26__all__ = ["JobFailedError", "JobTimeoutError", "async_wait_for_job", "wait_for_job"]
 27
 28import asyncio
 29import logging
 30import time
 31from typing import TYPE_CHECKING
 32
 33from .api.default import get_job
 34from .exceptions import IonQError
 35from .types import Unset
 36
 37if TYPE_CHECKING:
 38    from .client import AuthenticatedClient
 39    from .models.multi_circuit_job import MultiCircuitJob
 40    from .models.qaoa_job import QaoaJob
 41    from .models.quantum_function_job import QuantumFunctionJob
 42    from .models.single_circuit_job import SingleCircuitJob
 43
 44    # The spec's GetJobResponse component is an anyOf union, which the
 45    # generator inlines instead of emitting as a model class.
 46    GetJobResponse = SingleCircuitJob | MultiCircuitJob | QaoaJob | QuantumFunctionJob
 47
 48logger = logging.getLogger("ionq_core")
 49
 50_TERMINAL = frozenset({"completed", "failed", "canceled"})
 51_DEFAULT_INTERVAL = 1.0
 52_DEFAULT_TIMEOUT = 300.0
 53_MAX_INTERVAL = 30.0
 54_BACKOFF_FACTOR = 1.5
 55
 56
 57class JobTimeoutError(IonQError):
 58    """Raised when a job does not reach a terminal state within the timeout.
 59
 60    Attributes:
 61        job_id: The ID of the job that timed out.
 62        timeout: The timeout value in seconds that was exceeded.
 63        last_status: The last observed status before the timeout
 64            (e.g. ``"running"``, ``"submitted"``).
 65    """
 66
 67    def __init__(self, job_id: str, timeout: float, last_status: str) -> None:
 68        self.job_id = job_id
 69        self.timeout = timeout
 70        self.last_status = last_status
 71        super().__init__(f"Job {job_id} did not complete within {timeout}s (last status: {last_status})")
 72
 73
 74class JobFailedError(IonQError):
 75    """Raised when a polled job reaches ``"failed"`` status.
 76
 77    Attributes:
 78        job_id: The ID of the failed job.
 79        failure: The failure detail object from the API response, or ``None``
 80            if no failure details were provided.
 81    """
 82
 83    def __init__(self, job_id: str, failure: object) -> None:
 84        self.job_id = job_id
 85        self.failure = failure
 86        super().__init__(f"Job {job_id} failed: {failure}")
 87
 88
 89def _check_terminal(job: GetJobResponse, raise_on_failure: bool) -> bool:
 90    if job.status not in _TERMINAL:
 91        return False
 92    if raise_on_failure and job.status == "failed":
 93        failure = job.failure if not isinstance(job.failure, Unset) else None
 94        raise JobFailedError(job.id, failure)
 95    return True
 96
 97
 98def wait_for_job(
 99    client: AuthenticatedClient,
100    job_id: str,
101    *,
102    poll_interval: float = _DEFAULT_INTERVAL,
103    timeout: float = _DEFAULT_TIMEOUT,
104    raise_on_failure: bool = True,
105) -> GetJobResponse:
106    """Poll a job until it reaches a terminal state.
107
108    Terminal states are ``"completed"``, ``"failed"``, and ``"canceled"``.
109    Polling starts at ``poll_interval`` and increases by 1.5x each
110    iteration, capped at 30 seconds.
111
112    Args:
113        client: An authenticated API client.
114        job_id: The UUID of the job to poll.
115        poll_interval: Initial interval between polls in seconds.
116            Defaults to 1.0.
117        timeout: Maximum total wait time in seconds. Defaults to 300
118            (5 minutes).
119        raise_on_failure: If ``True`` (the default), raise `JobFailedError`
120            when the job status is ``"failed"``. If ``False``, return the
121            failed job response instead.
122
123    Returns:
124        The final job response once a terminal state is reached.
125
126    Raises:
127        JobTimeoutError: If the job does not finish within ``timeout``.
128        JobFailedError: If ``raise_on_failure`` is ``True`` and the job fails.
129        IonQError: If the API returns a ``None`` response.
130    """
131    deadline = time.monotonic() + timeout
132    interval = poll_interval
133    while True:
134        job = get_job.sync(uuid=job_id, client=client)
135        if job is None:
136            raise IonQError(f"Failed to fetch job {job_id}")
137        logger.debug("Job %s status: %s", job_id, job.status)
138        if _check_terminal(job, raise_on_failure):
139            return job
140        if time.monotonic() >= deadline:
141            raise JobTimeoutError(job_id, timeout, job.status)
142        time.sleep(max(0, min(interval, deadline - time.monotonic())))
143        interval = min(interval * _BACKOFF_FACTOR, _MAX_INTERVAL)
144
145
146async def async_wait_for_job(
147    client: AuthenticatedClient,
148    job_id: str,
149    *,
150    poll_interval: float = _DEFAULT_INTERVAL,
151    timeout: float = _DEFAULT_TIMEOUT,
152    raise_on_failure: bool = True,
153) -> GetJobResponse:
154    """Async version of `wait_for_job`.
155
156    Args:
157        client: An authenticated API client.
158        job_id: The UUID of the job to poll.
159        poll_interval: Initial interval between polls in seconds.
160            Defaults to 1.0.
161        timeout: Maximum total wait time in seconds. Defaults to 300.
162        raise_on_failure: If ``True``, raise `JobFailedError` on failure.
163
164    Returns:
165        The final job response once a terminal state is reached.
166
167    Raises:
168        JobTimeoutError: If the job does not finish within ``timeout``.
169        JobFailedError: If ``raise_on_failure`` is ``True`` and the job fails.
170        IonQError: If the API returns a ``None`` response.
171    """
172    deadline = time.monotonic() + timeout
173    interval = poll_interval
174    while True:
175        job = await get_job.asyncio(uuid=job_id, client=client)
176        if job is None:
177            raise IonQError(f"Failed to fetch job {job_id}")
178        logger.debug("Job %s status: %s", job_id, job.status)
179        if _check_terminal(job, raise_on_failure):
180            return job
181        if time.monotonic() >= deadline:
182            raise JobTimeoutError(job_id, timeout, job.status)
183        await asyncio.sleep(max(0, min(interval, deadline - time.monotonic())))
184        interval = min(interval * _BACKOFF_FACTOR, _MAX_INTERVAL)
class JobFailedError(ionq_core.exceptions.IonQError):
75class JobFailedError(IonQError):
76    """Raised when a polled job reaches ``"failed"`` status.
77
78    Attributes:
79        job_id: The ID of the failed job.
80        failure: The failure detail object from the API response, or ``None``
81            if no failure details were provided.
82    """
83
84    def __init__(self, job_id: str, failure: object) -> None:
85        self.job_id = job_id
86        self.failure = failure
87        super().__init__(f"Job {job_id} failed: {failure}")

Raised when a polled job reaches "failed" status.

Attributes:
  • job_id: The ID of the failed job.
  • failure: The failure detail object from the API response, or None if no failure details were provided.
JobFailedError(job_id: str, failure: object)
84    def __init__(self, job_id: str, failure: object) -> None:
85        self.job_id = job_id
86        self.failure = failure
87        super().__init__(f"Job {job_id} failed: {failure}")
job_id
failure
class JobTimeoutError(ionq_core.exceptions.IonQError):
58class JobTimeoutError(IonQError):
59    """Raised when a job does not reach a terminal state within the timeout.
60
61    Attributes:
62        job_id: The ID of the job that timed out.
63        timeout: The timeout value in seconds that was exceeded.
64        last_status: The last observed status before the timeout
65            (e.g. ``"running"``, ``"submitted"``).
66    """
67
68    def __init__(self, job_id: str, timeout: float, last_status: str) -> None:
69        self.job_id = job_id
70        self.timeout = timeout
71        self.last_status = last_status
72        super().__init__(f"Job {job_id} did not complete within {timeout}s (last status: {last_status})")

Raised when a job does not reach a terminal state within the timeout.

Attributes:
  • job_id: The ID of the job that timed out.
  • timeout: The timeout value in seconds that was exceeded.
  • last_status: The last observed status before the timeout (e.g. "running", "submitted").
JobTimeoutError(job_id: str, timeout: float, last_status: str)
68    def __init__(self, job_id: str, timeout: float, last_status: str) -> None:
69        self.job_id = job_id
70        self.timeout = timeout
71        self.last_status = last_status
72        super().__init__(f"Job {job_id} did not complete within {timeout}s (last status: {last_status})")
job_id
timeout
last_status
async def async_wait_for_job( client: ionq_core.AuthenticatedClient, job_id: str, *, poll_interval: float = 1.0, timeout: float = 300.0, raise_on_failure: bool = True) -> ionq_core.models.single_circuit_job.SingleCircuitJob | ionq_core.models.multi_circuit_job.MultiCircuitJob | ionq_core.models.qaoa_job.QaoaJob | ionq_core.models.quantum_function_job.QuantumFunctionJob:
147async def async_wait_for_job(
148    client: AuthenticatedClient,
149    job_id: str,
150    *,
151    poll_interval: float = _DEFAULT_INTERVAL,
152    timeout: float = _DEFAULT_TIMEOUT,
153    raise_on_failure: bool = True,
154) -> GetJobResponse:
155    """Async version of `wait_for_job`.
156
157    Args:
158        client: An authenticated API client.
159        job_id: The UUID of the job to poll.
160        poll_interval: Initial interval between polls in seconds.
161            Defaults to 1.0.
162        timeout: Maximum total wait time in seconds. Defaults to 300.
163        raise_on_failure: If ``True``, raise `JobFailedError` on failure.
164
165    Returns:
166        The final job response once a terminal state is reached.
167
168    Raises:
169        JobTimeoutError: If the job does not finish within ``timeout``.
170        JobFailedError: If ``raise_on_failure`` is ``True`` and the job fails.
171        IonQError: If the API returns a ``None`` response.
172    """
173    deadline = time.monotonic() + timeout
174    interval = poll_interval
175    while True:
176        job = await get_job.asyncio(uuid=job_id, client=client)
177        if job is None:
178            raise IonQError(f"Failed to fetch job {job_id}")
179        logger.debug("Job %s status: %s", job_id, job.status)
180        if _check_terminal(job, raise_on_failure):
181            return job
182        if time.monotonic() >= deadline:
183            raise JobTimeoutError(job_id, timeout, job.status)
184        await asyncio.sleep(max(0, min(interval, deadline - time.monotonic())))
185        interval = min(interval * _BACKOFF_FACTOR, _MAX_INTERVAL)

Async version of wait_for_job.

Arguments:
  • client: An authenticated API client.
  • job_id: The UUID of the job to poll.
  • poll_interval: Initial interval between polls in seconds. Defaults to 1.0.
  • timeout: Maximum total wait time in seconds. Defaults to 300.
  • raise_on_failure: If True, raise JobFailedError on failure.
Returns:

The final job response once a terminal state is reached.

Raises:
  • JobTimeoutError: If the job does not finish within timeout.
  • JobFailedError: If raise_on_failure is True and the job fails.
  • IonQError: If the API returns a None response.
def wait_for_job( client: ionq_core.AuthenticatedClient, job_id: str, *, poll_interval: float = 1.0, timeout: float = 300.0, raise_on_failure: bool = True) -> ionq_core.models.single_circuit_job.SingleCircuitJob | ionq_core.models.multi_circuit_job.MultiCircuitJob | ionq_core.models.qaoa_job.QaoaJob | ionq_core.models.quantum_function_job.QuantumFunctionJob:
 99def wait_for_job(
100    client: AuthenticatedClient,
101    job_id: str,
102    *,
103    poll_interval: float = _DEFAULT_INTERVAL,
104    timeout: float = _DEFAULT_TIMEOUT,
105    raise_on_failure: bool = True,
106) -> GetJobResponse:
107    """Poll a job until it reaches a terminal state.
108
109    Terminal states are ``"completed"``, ``"failed"``, and ``"canceled"``.
110    Polling starts at ``poll_interval`` and increases by 1.5x each
111    iteration, capped at 30 seconds.
112
113    Args:
114        client: An authenticated API client.
115        job_id: The UUID of the job to poll.
116        poll_interval: Initial interval between polls in seconds.
117            Defaults to 1.0.
118        timeout: Maximum total wait time in seconds. Defaults to 300
119            (5 minutes).
120        raise_on_failure: If ``True`` (the default), raise `JobFailedError`
121            when the job status is ``"failed"``. If ``False``, return the
122            failed job response instead.
123
124    Returns:
125        The final job response once a terminal state is reached.
126
127    Raises:
128        JobTimeoutError: If the job does not finish within ``timeout``.
129        JobFailedError: If ``raise_on_failure`` is ``True`` and the job fails.
130        IonQError: If the API returns a ``None`` response.
131    """
132    deadline = time.monotonic() + timeout
133    interval = poll_interval
134    while True:
135        job = get_job.sync(uuid=job_id, client=client)
136        if job is None:
137            raise IonQError(f"Failed to fetch job {job_id}")
138        logger.debug("Job %s status: %s", job_id, job.status)
139        if _check_terminal(job, raise_on_failure):
140            return job
141        if time.monotonic() >= deadline:
142            raise JobTimeoutError(job_id, timeout, job.status)
143        time.sleep(max(0, min(interval, deadline - time.monotonic())))
144        interval = min(interval * _BACKOFF_FACTOR, _MAX_INTERVAL)

Poll a job until it reaches a terminal state.

Terminal states are "completed", "failed", and "canceled". Polling starts at poll_interval and increases by 1.5x each iteration, capped at 30 seconds.

Arguments:
  • client: An authenticated API client.
  • job_id: The UUID of the job to poll.
  • poll_interval: Initial interval between polls in seconds. Defaults to 1.0.
  • timeout: Maximum total wait time in seconds. Defaults to 300 (5 minutes).
  • raise_on_failure: If True (the default), raise JobFailedError when the job status is "failed". If False, return the failed job response instead.
Returns:

The final job response once a terminal state is reached.

Raises:
  • JobTimeoutError: If the job does not finish within timeout.
  • JobFailedError: If raise_on_failure is True and the job fails.
  • IonQError: If the API returns a None response.