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
70 changes: 69 additions & 1 deletion custom-templates/package_init.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,75 @@
# Copyright 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

{{ safe_docstring(package_description) }}
"""A Python client library for the [IonQ Cloud Platform API](https://docs.ionq.com/).

Provides full access to IonQ's quantum computing services with typed models for all
request and response objects. Supports both synchronous and asynchronous usage.

## Quick start

```python
from ionq_core import IonQClient
from ionq_core.api.backends import get_backends

# Authenticate with the IONQ_API_KEY environment variable
client = IonQClient()

# List available quantum backends
for backend in get_backends.sync(client=client):
print(f"{backend.backend}: {backend.status}")
```

## Authentication

Get an API key from the [IonQ Cloud Console](https://cloud.ionq.com), then
either set the ``IONQ_API_KEY`` environment variable or pass it directly:

```python
client = IonQClient() # reads IONQ_API_KEY
client = IonQClient(api_key="your-key") # explicit key
```

## Submitting a job

```python
from ionq_core.api.default import create_job
from ionq_core.models.circuit_job_creation_payload import CircuitJobCreationPayload

job = create_job.sync(
client=client,
body=CircuitJobCreationPayload.from_dict({
"type": "ionq.circuit.v1",
"backend": "simulator",
"shots": 1000,
"input": {
"gateset": "qis",
"circuit": [
{"gate": "h", "targets": [0]},
{"gate": "cnot", "targets": [0], "controls": [1]},
],
},
}),
)
```

## Key features

- **Sync and async** - every endpoint has ``.sync()`` and ``.asyncio()`` variants.
- **Automatic retries** - transient errors (429, 5xx) are retried with exponential
backoff. See `IonQClient` for configuration.
- **Typed exceptions** - HTTP errors are raised as `AuthenticationError`,
`RateLimitError`, `ServerError`, etc. See `_exceptions` for the full hierarchy.
- **Pagination helpers** - `iter_jobs` and `aiter_jobs` follow cursors automatically.
- **Job polling** - `wait_for_job` and `async_wait_for_job` poll until completion.
- **Session management** - `SessionManager` wraps the session lifecycle as a
context manager.
- **Native gate matrices** - `gpi_matrix`, `gpi2_matrix`, `ms_matrix`, and
`zz_matrix` return pure-Python unitary matrices for simulation and verification.
- **Extensibility** - `ClientExtension` lets downstream SDKs inject hooks, headers,
custom transports, and error mappers without modifying this library.
"""

from ._exceptions import (
APIConnectionError,
APIError,
Expand Down
71 changes: 70 additions & 1 deletion ionq_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,76 @@
# Copyright 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

"""A client library for accessing IonQ Cloud Platform API"""
"""A Python client library for the [IonQ Cloud Platform API](https://docs.ionq.com/).

Provides full access to IonQ's quantum computing services with typed models for all
request and response objects. Supports both synchronous and asynchronous usage.

## Quick start

```python
from ionq_core import IonQClient
from ionq_core.api.backends import get_backends

# Authenticate with the IONQ_API_KEY environment variable
client = IonQClient()

# List available quantum backends
for backend in get_backends.sync(client=client):
print(f"{backend.backend}: {backend.status}")
```

## Authentication

Get an API key from the [IonQ Cloud Console](https://cloud.ionq.com), then
either set the ``IONQ_API_KEY`` environment variable or pass it directly:

```python
client = IonQClient() # reads IONQ_API_KEY
client = IonQClient(api_key="your-key") # explicit key
```

## Submitting a job

```python
from ionq_core.api.default import create_job
from ionq_core.models.circuit_job_creation_payload import CircuitJobCreationPayload

job = create_job.sync(
client=client,
body=CircuitJobCreationPayload.from_dict(
{
"type": "ionq.circuit.v1",
"backend": "simulator",
"shots": 1000,
"input": {
"gateset": "qis",
"circuit": [
{"gate": "h", "targets": [0]},
{"gate": "cnot", "targets": [0], "controls": [1]},
],
},
}
),
)
```

## Key features

- **Sync and async** - every endpoint has ``.sync()`` and ``.asyncio()`` variants.
- **Automatic retries** - transient errors (429, 5xx) are retried with exponential
backoff. See `IonQClient` for configuration.
- **Typed exceptions** - HTTP errors are raised as `AuthenticationError`,
`RateLimitError`, `ServerError`, etc. See `_exceptions` for the full hierarchy.
- **Pagination helpers** - `iter_jobs` and `aiter_jobs` follow cursors automatically.
- **Job polling** - `wait_for_job` and `async_wait_for_job` poll until completion.
- **Session management** - `SessionManager` wraps the session lifecycle as a
context manager.
- **Native gate matrices** - `gpi_matrix`, `gpi2_matrix`, `ms_matrix`, and
`zz_matrix` return pure-Python unitary matrices for simulation and verification.
- **Extensibility** - `ClientExtension` lets downstream SDKs inject hooks, headers,
custom transports, and error mappers without modifying this library.
"""

from ._exceptions import (
APIConnectionError,
Expand Down
122 changes: 111 additions & 11 deletions ionq_core/_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,75 @@
# Copyright 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Structured exceptions for the IonQ API client."""
"""Structured exceptions for the IonQ API client.

All exceptions inherit from `IonQError`. The hierarchy is:

```
IonQError
+-- APIConnectionError # network / DNS failures
| +-- APITimeoutError # request timed out
+-- APIError # HTTP 4xx / 5xx responses
| +-- BadRequestError # 400
| +-- AuthenticationError # 401
| +-- PermissionDeniedError # 403
| +-- NotFoundError # 404
| +-- RateLimitError # 429 (includes retry_after)
| +-- ServerError # 5xx
```

Example:
```python
from ionq_core import IonQClient, RateLimitError, AuthenticationError

client = IonQClient()
try:
job = create_job.sync(client=client, body=payload)
except AuthenticationError:
print("Invalid API key")
except RateLimitError as e:
print(f"Rate limited, retry after {e.retry_after}s")
```
"""


class IonQError(Exception):
"""Base exception for all IonQ API errors."""
"""Base exception for all IonQ errors.

Catch this to handle any error raised by the library, including connection
failures, API errors, polling timeouts, and job failures.
"""


class APIConnectionError(IonQError):
"""Raised when a connection to the IonQ API cannot be established."""
"""Raised when a connection to the IonQ API cannot be established.

This covers DNS resolution failures, refused connections, and other
network-level errors. The original ``httpx`` exception is chained
via ``__cause__``.
"""


class APITimeoutError(APIConnectionError):
"""Raised when a request to the IonQ API times out."""
"""Raised when a request to the IonQ API times out.

Inherits from `APIConnectionError` so that catching connection errors
also catches timeouts.
"""


class APIError(IonQError):
"""Raised when the IonQ API returns an error response."""
"""Raised when the IonQ API returns an HTTP error response (4xx or 5xx).

Attributes:
status_code: The HTTP status code.
body: The parsed response body (``dict`` if JSON, ``str`` otherwise,
or ``None`` if the body could not be read).
message: A human-readable error message extracted from the response,
or a default ``"HTTP <status>"`` string.
request_id: The ``x-request-id`` header from the response, useful for
contacting IonQ support about a specific request.
"""

def __init__(
self,
Expand All @@ -35,23 +87,45 @@ def __init__(


class AuthenticationError(APIError):
"""Raised on 401 Unauthorized."""
"""Raised on ``401 Unauthorized``.

Typically means the API key is missing, invalid, or revoked.
"""


class PermissionDeniedError(APIError):
"""Raised on 403 Forbidden."""
"""Raised on ``403 Forbidden``.

The API key is valid but lacks permission for the requested operation.
"""


class NotFoundError(APIError):
"""Raised on 404 Not Found."""
"""Raised on ``404 Not Found``.

The requested resource (job, session, backend, etc.) does not exist.
"""


class BadRequestError(APIError):
"""Raised on 400 Bad Request."""
"""Raised on ``400 Bad Request``.

The request body or query parameters failed server-side validation.
Inspect ``body`` for details.
"""


class RateLimitError(APIError):
"""Raised on 429 Too Many Requests."""
"""Raised on ``429 Too Many Requests``.

The client has exceeded the API rate limit. The ``retry_after`` attribute
indicates how many seconds to wait before retrying, if the server provided
a ``Retry-After`` header.

Attributes:
retry_after: Seconds to wait before retrying, or ``None`` if the
server did not include a ``Retry-After`` header.
"""

def __init__(
self,
Expand All @@ -67,7 +141,11 @@ def __init__(


class ServerError(APIError):
"""Raised on 5xx server errors."""
"""Raised on ``5xx`` server errors.

These are typically transient and are automatically retried by the default
transport (see `IonQClient`).
"""


_STATUS_TO_EXCEPTION: dict[int, type[APIError]] = {
Expand All @@ -87,6 +165,28 @@ def raise_for_status(
*,
request_id: str | None = None,
) -> None:
"""Raise an appropriate `APIError` subclass for an HTTP error status.

Does nothing for status codes below 400. For 4xx codes, raises the
specific subclass (e.g. `AuthenticationError` for 401). For 5xx codes
or unrecognized 4xx codes, raises `ServerError` or `APIError` respectively.

Args:
status_code: The HTTP status code.
body: The parsed response body.
retry_after: Value from the ``Retry-After`` header, if present.
message: A human-readable error message.
request_id: The ``x-request-id`` response header.

Raises:
BadRequestError: On 400.
AuthenticationError: On 401.
PermissionDeniedError: On 403.
NotFoundError: On 404.
RateLimitError: On 429.
ServerError: On 5xx.
APIError: On other 4xx codes.
"""
if status_code < 400:
return
exc_cls = _STATUS_TO_EXCEPTION.get(status_code, ServerError if status_code >= 500 else APIError)
Expand Down
Loading
Loading