Skip to content
Open
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- `examples/` directory with sync and async downstream-SDK integration scripts demonstrating the extension API against the free `simulator`.
- `QctrlQaoaJobCreationPayload` and `QctrlQaoaJobInput` for submitting Q-CTRL QAOA maxcut combinatorial-optimization jobs via `create_job`. The `create_job` body union now also accepts `QctrlQaoaJobCreationPayload`.
- `cost_model` optional field on `BaseJob`, `GetCircuitJobResponse`, and `GetJobResponse`, typed as `CostModel` (`"quantum_compute_time"` or `"execution_time"`).
- `cost_model` optional field on `BaseJob`, `GetCircuitJobResponse`, and `GetJobResponse`, typed as `CostModel` (`"quantum_compute_time"`, `"execution_time"`, `"QCT"`, or `"2QGE_operations"`).

### Changed

- Extended the local OpenAPI overlay so `CostModel` accepts live API values (`QCT`, `2QGE_operations`) returned by completed jobs; fixes `get_job` deserialization failures during polling.
- `NativeCircuitInput.qubits` and `JsonMultiCircuitInput.qubits` are now `int | Unset` (previously `float | Unset`), matching upstream's tightening to `format: int32, minimum: 1`. `QisCircuitInput.qubits` already had this type locally via the OpenAPI overlay; that overlay action has been removed now that upstream is correct natively.

## [0.1.1] - 2026-04-30
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ Each generated endpoint module exposes four callables: `sync`, `sync_detailed`,

For options (`api_key`, `base_url`, `max_retries`, `timeout`, `extension`), error classes, retry behavior, pagination, polling, sessions, and downstream-SDK extension hooks, see the [API reference](https://ionq.github.io/ionq-core-python/).

## Examples

Runnable scripts for downstream SDK integration live in [`examples/`](examples/):

- [`downstream_integration.py`](examples/downstream_integration.py) — sync `ClientExtension` integration (event hooks, error mapping, Bell-state job).
- [`downstream_integration_async.py`](examples/downstream_integration_async.py) — the same flow on the async path.

See [`examples/README.md`](examples/README.md) for setup (`pip install ionq-core`, `export IONQ_API_KEY=...`).

## Versioning

This package follows [SemVer 2.0](https://semver.org/spec/v2.0.0.html), independent of the upstream REST API version - pass an explicit `base_url` to `IonQClient` to pin against a different API. Print the installed version with:
Expand Down
49 changes: 49 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ionq-core examples

Runnable scripts that demonstrate how downstream SDKs integrate with `ionq-core`
through the [extension API](https://ionq.github.io/ionq-core-python/ionq_core/extensions.html).

## Setup

```sh
pip install ionq-core
export IONQ_API_KEY=your-api-key
```

Create a free account at [identity.ionq.com/create-account](https://identity.ionq.com/create-account) if you need an API key. Both scripts target the free `simulator` backend.

On Windows PowerShell:

```powershell
$env:IONQ_API_KEY = "your-api-key"
python examples/downstream_integration.py
```

From a clone of this repository you can also use:

```sh
uv run python examples/downstream_integration.py
```

## Scripts

| Script | What it demonstrates |
| --- | --- |
| [`downstream_integration.py`](downstream_integration.py) | Sync `ClientExtension`: `user_agent_token`, `default_headers`, `EventHook`, `error_mapper`, Bell-state job lifecycle |
| [`downstream_integration_async.py`](downstream_integration_async.py) | Same flow with `AsyncEventHook`, `async_wait_for_job`, and `asyncio` endpoints |

Each script configures a `ClientExtension` with:

- `user_agent_token` — downstream SDK identity in the `User-Agent` header
- `default_headers` — SDK-specific headers on every request
- `EventHook` / `AsyncEventHook` — request, response, and error logging
- `error_mapper` — wraps `APIError` and `RateLimitError` (including `request_id`) into SDK types when a real API call fails

After submitting a Bell-state circuit and waiting for completion, the scripts print
the job id, status, and measured probabilities (expect roughly equal weight on
`|00⟩` and `|11⟩`).

```sh
python examples/downstream_integration.py
python examples/downstream_integration_async.py
```
Binary file not shown.
Binary file not shown.
Binary file added examples/__pycache__/sdk_shared.cpython-311.pyc
Binary file not shown.
135 changes: 135 additions & 0 deletions examples/downstream_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# SPDX-FileCopyrightText: 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Sync downstream-SDK example: ClientExtension + Bell state on simulator.

Shows how a higher-level SDK wraps ionq-core with ``user_agent_token``,
``default_headers``, an ``EventHook``, and an ``error_mapper`` that translates
``APIError`` / ``RateLimitError`` into SDK-defined exception types.

Extension API reference:
https://ionq.github.io/ionq-core-python/ionq_core/extensions.html
"""

from __future__ import annotations

import logging
import sys

import httpx

from ionq_core import APIError, ClientExtension, EventHook, IonQClient, RateLimitError, wait_for_job
from ionq_core.api.default import create_job, get_job_probabilities
from ionq_core.models.circuit_job_creation_payload import CircuitJobCreationPayload

SDK_NAME = "example-sdk/0.1.0"
logger = logging.getLogger(__name__)

BELL_CIRCUIT = CircuitJobCreationPayload.from_dict(
{
"type": "ionq.circuit.v1",
"backend": "simulator",
"shots": 100,
"input": {
"gateset": "qis",
"qubits": 2,
"circuit": [
{"gate": "h", "target": 0},
{"gate": "cnot", "control": 0, "target": 1},
],
},
}
)


class ExampleSDKError(Exception):
"""Base exception for this example downstream SDK."""


class ExampleAPIError(ExampleSDKError):
"""SDK wrapper around ionq-core HTTP API errors."""

def __init__(
self,
message: str,
*,
status_code: int | None = None,
request_id: str | None = None,
) -> None:
self.status_code = status_code
self.request_id = request_id
super().__init__(message)


class ExampleRateLimitError(ExampleAPIError):
"""SDK wrapper around ionq-core rate-limit errors."""

def __init__(self, message: str, *, retry_after: float | None = None) -> None:
self.retry_after = retry_after
super().__init__(message, status_code=429)


class LoggingHook(EventHook):
"""Sync event hook: log requests, responses, and transport failures."""

def on_request(self, request: httpx.Request) -> None:
logger.info("--> %s %s", request.method, request.url)

def on_response(self, request: httpx.Request, response: httpx.Response) -> None:
logger.info("<-- %s %s", response.status_code, request.url)

def on_error(self, request: httpx.Request, error: Exception) -> None:
logger.warning("!! %s %s failed: %s", request.method, request.url, error)


def map_ionq_error(exc: Exception) -> Exception:
if isinstance(exc, RateLimitError):
return ExampleRateLimitError(f"IonQ rate limit: {exc.message}", retry_after=exc.retry_after)
if isinstance(exc, APIError):
return ExampleAPIError(
f"IonQ API {exc.status_code}: {exc.message}",
status_code=exc.status_code,
request_id=exc.request_id,
)
return exc


def main() -> int:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logging.getLogger("httpx").setLevel(logging.WARNING)

extension = ClientExtension(
user_agent_token=SDK_NAME,
default_headers={"X-Example-SDK": SDK_NAME},
event_hooks=(LoggingHook(),),
error_mapper=map_ionq_error,
)

try:
with IonQClient(extension=extension) as client:
job = create_job.sync(client=client, body=BELL_CIRCUIT)
if job is None:
raise ExampleSDKError("create_job returned no job")

completed = wait_for_job(client, job.id, timeout=120)
probs = get_job_probabilities.sync(uuid=job.id, client=client)
if probs is None:
raise ExampleSDKError(f"get_job_probabilities returned no data for job {job.id}")

print()
print("Bell-state job on simulator")
print(f" job_id: {completed.id}")
print(f" status: {completed.status}")
print(f" results: {probs.additional_properties}")
except ExampleSDKError:
logger.exception("downstream SDK call failed")
return 1
except ValueError as exc:
logger.error("%s", exc)
return 1

return 0


if __name__ == "__main__":
sys.exit(main())
142 changes: 142 additions & 0 deletions examples/downstream_integration_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# SPDX-FileCopyrightText: 2026 IonQ, Inc.
# SPDX-License-Identifier: Apache-2.0

"""Async downstream-SDK example: ClientExtension + Bell state on simulator.

Same flow as ``downstream_integration.py`` using ``AsyncEventHook``,
``async_wait_for_job``, and the ``asyncio`` endpoint variants.

Extension API reference:
https://ionq.github.io/ionq-core-python/ionq_core/extensions.html
"""

from __future__ import annotations

import asyncio
import logging
import sys

import httpx

from ionq_core import (
APIError,
AsyncEventHook,
ClientExtension,
IonQClient,
RateLimitError,
async_wait_for_job,
)
from ionq_core.api.default import create_job, get_job_probabilities
from ionq_core.models.circuit_job_creation_payload import CircuitJobCreationPayload

SDK_NAME = "example-sdk/0.1.0"
logger = logging.getLogger(__name__)

BELL_CIRCUIT = CircuitJobCreationPayload.from_dict(
{
"type": "ionq.circuit.v1",
"backend": "simulator",
"shots": 100,
"input": {
"gateset": "qis",
"qubits": 2,
"circuit": [
{"gate": "h", "target": 0},
{"gate": "cnot", "control": 0, "target": 1},
],
},
}
)


class ExampleSDKError(Exception):
"""Base exception for this example downstream SDK."""


class ExampleAPIError(ExampleSDKError):
"""SDK wrapper around ionq-core HTTP API errors."""

def __init__(
self,
message: str,
*,
status_code: int | None = None,
request_id: str | None = None,
) -> None:
self.status_code = status_code
self.request_id = request_id
super().__init__(message)


class ExampleRateLimitError(ExampleAPIError):
"""SDK wrapper around ionq-core rate-limit errors."""

def __init__(self, message: str, *, retry_after: float | None = None) -> None:
self.retry_after = retry_after
super().__init__(message, status_code=429)


class AsyncLoggingHook(AsyncEventHook):
"""Async event hook: log requests, responses, and transport failures."""

async def on_request(self, request: httpx.Request) -> None:
logger.info("--> %s %s", request.method, request.url)

async def on_response(self, request: httpx.Request, response: httpx.Response) -> None:
logger.info("<-- %s %s", response.status_code, request.url)

async def on_error(self, request: httpx.Request, error: Exception) -> None:
logger.warning("!! %s %s failed: %s", request.method, request.url, error)


def map_ionq_error(exc: Exception) -> Exception:
if isinstance(exc, RateLimitError):
return ExampleRateLimitError(f"IonQ rate limit: {exc.message}", retry_after=exc.retry_after)
if isinstance(exc, APIError):
return ExampleAPIError(
f"IonQ API {exc.status_code}: {exc.message}",
status_code=exc.status_code,
request_id=exc.request_id,
)
return exc


async def main() -> int:
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logging.getLogger("httpx").setLevel(logging.WARNING)

extension = ClientExtension(
user_agent_token=SDK_NAME,
default_headers={"X-Example-SDK": SDK_NAME},
async_event_hooks=(AsyncLoggingHook(),),
error_mapper=map_ionq_error,
)

try:
async with IonQClient(extension=extension) as client:
job = await create_job.asyncio(client=client, body=BELL_CIRCUIT)
if job is None:
raise ExampleSDKError("create_job returned no job")

completed = await async_wait_for_job(client, job.id, timeout=120)
probs = await get_job_probabilities.asyncio(uuid=job.id, client=client)
if probs is None:
raise ExampleSDKError(f"get_job_probabilities returned no data for job {job.id}")

print()
print("Bell-state job on simulator")
print(f" job_id: {completed.id}")
print(f" status: {completed.status}")
print(f" results: {probs.additional_properties}")
except ExampleSDKError:
logger.exception("downstream SDK call failed")
return 1
except ValueError as exc:
logger.error("%s", exc)
return 1

return 0


if __name__ == "__main__":
sys.exit(asyncio.run(main()))
4 changes: 2 additions & 2 deletions ionq_core/models/cost_model.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading