From 18207b969f8f76277a254466eb01fbd4067f9091 Mon Sep 17 00:00:00 2001 From: Rafsanul Islam Neloy <89649374+RafsanNeloy@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:18:10 +0600 Subject: [PATCH] Add downstream SDK integration examples --- CHANGELOG.md | 4 +- README.md | 9 ++ examples/README.md | 49 ++++++ .../downstream_integration.cpython-311.pyc | Bin 0 -> 5049 bytes ...wnstream_integration_async.cpython-311.pyc | Bin 0 -> 5313 bytes .../__pycache__/sdk_shared.cpython-311.pyc | Bin 0 -> 5783 bytes examples/downstream_integration.py | 135 +++++++++++++++++ examples/downstream_integration_async.py | 142 ++++++++++++++++++ ionq_core/models/cost_model.py | 4 +- openapi-overlay.yaml | 14 +- 10 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/__pycache__/downstream_integration.cpython-311.pyc create mode 100644 examples/__pycache__/downstream_integration_async.cpython-311.pyc create mode 100644 examples/__pycache__/sdk_shared.cpython-311.pyc create mode 100644 examples/downstream_integration.py create mode 100644 examples/downstream_integration_async.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c655fdb..eea8945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 992bbde..69b8437 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..21047f2 --- /dev/null +++ b/examples/README.md @@ -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 +``` diff --git a/examples/__pycache__/downstream_integration.cpython-311.pyc b/examples/__pycache__/downstream_integration.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74a597d59a254c04033e88b23253699b7c8edb47 GIT binary patch literal 5049 zcmcInU2NOd6}}WHiIQm9j{jok$8?;gQEbcWwq%K$7PDQu4bmD-Tm*@>l%_@6rcIH` zB^Af64G2RX+#II4xvy zxlks=VVO7i49~toMquA?CJbM}jO3!3Xim(CxmYHai)Z4wL?)4wGE%N3)57|O&19}M z)0%6`wB_0}?YRS)1G$b&2dj&iow=?|SFStL&5;n%`Q@IPQ2N39AkD|{23MvxL|!FY z^fu8%z5PB9b?{c}`m_UD;Ej5=n^)Y$UG<3_Iy5;JY zW7w8FBv0t3DLby}>N5O0My_D0u1(YN`0SFs>c~K3RbDa{mvlN}>Nj;$#t~Pkns;Ov z;>JkUrnC^$HtXj}|>LxUN z&9+wpjuvorN^Pp-)O=p2b91u0q`I<8Rm%avbO(kTfAhi=wt>n~809jsUpI1wTVooQ zYom}OFt1_h8Z1An=UrT{yO!4-TqvlzF9 zSk~QaIt3cp(;N1r`hre%E31#ims~gRjE#<>KGKVzio$%_u-PwD&q#jFU9v64P*7i^ zdd&n*ddbb1@vqRlDb7!*mSuxAU?gWI74=0}hcYw%#+2VW6&Mj1Y@z;vc`%*AOsC}4 zHPco#zc*`J3&tWWNU7RfG3~`g!&>x{piu<{RG5POmOKSTDX?@fjpMgc{YJrnern!O zKn1C=-!Wcrm%u_s7L=g{(D7UMLCBBQt?0)u*w&?LJ84xlTx9_)xopp;LVi38DtC4K z^Sc&x7Yj89r@bdxDNrr>{9@042-$^Y3dq&V&wHSQE zTAL=pH=?!7l2p5&cs*D!;%a@I?FK-tgfatUeE#)}g3cS^%@W zP2D;y(tfns`bi5~`BwUJ(Aker-7Qed7iMfr&sODSOF%ybd!BoInaS=lWuQxZU&N(NB6lJya53DvK|9wdXOkBEDEt_LF2ylY#QH_2ojd|3-PC|4@17 zM@DMOqrEUE?L+wzxRnYH>`!n~hk=vg=@G0&<6Evz?migzw38$hnsSrI2|GqG9YisL$dx-~xdkoo-5n8qk`Nvncpcy1xQP;@n zcEQDPStuA9>%Knv%q*p83{7=a8FCyPOismU8=KaF44mTLpr|?8ip&s<+Kz=t5M}%> zRns8tu`SiaGzfBC%z?nOG0}NUlVVvLvv$b6{4i!pj)P!^_)~>#k`^pwFA1d6P!mcz>mQFmFd^r8t8BdxhNfTvhqByk^m5Q5efUDk{ zv*kChct@|oQ%YVdC$D+pwMU80JLm89mU~kVPM3$Ky~MO9Ob1(mhg9R)p6LfqfxOw{ zIFSC$5*NZ>?P{*)NbFy1h0_FlQXMwvlbV75a~OA%+YD{;O&fsQ2yO7qpvORocQdFt zVkHL23&astL$~;AWR>H{HNwGd4Gj{P9cu%*j}v;7BQCn9>b2b*K z7%53-%hFj-I$H@tW7dO_ud(kpNcMQoDt-6ech6>j8zIsms0z355lCzQCLa7kJm~d8 z9&)BEp7F#pYzbR8VE%ABjwTYRW$Oj!$FSP%P{O27K6hH45#s{OH5As+( z!D0D%RvtP&(ZT=u00$IP4%C~XfY+)=?yB-BM)81q4obBauVT5MV&;T-%0dC08ssSy zH-;a@!vLQ5cxTP=!z|PG;1N0$Sd_liuxO5bmG7?O# zpNQv!^y_7M6@C%n&igpgC057_&g%XBD;4;yvXiHL@Kho5)!#k zh5SEAC*kHek0}4nvqL7mXFZQ%eOo={Sl_*~rP%Ru?07L!iNra~`JPShroLu1-!#_u zMY*90X(pX+!uif>fNm{o2-BTW`I4<<6Co&{-Bb Pz1maZiO>hFnAQFXhknPg literal 0 HcmV?d00001 diff --git a/examples/__pycache__/downstream_integration_async.cpython-311.pyc b/examples/__pycache__/downstream_integration_async.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b29d075eda20eab4cb588ae34bd3058e47691e80 GIT binary patch literal 5313 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 VB)kcsAwXhO*a(ULiwTF|{{UY28;$?~ literal 0 HcmV?d00001 diff --git a/examples/downstream_integration.py b/examples/downstream_integration.py new file mode 100644 index 0000000..5c14b64 --- /dev/null +++ b/examples/downstream_integration.py @@ -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()) diff --git a/examples/downstream_integration_async.py b/examples/downstream_integration_async.py new file mode 100644 index 0000000..e4ed2c0 --- /dev/null +++ b/examples/downstream_integration_async.py @@ -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())) diff --git a/ionq_core/models/cost_model.py b/ionq_core/models/cost_model.py index c464277..3348e36 100644 --- a/ionq_core/models/cost_model.py +++ b/ionq_core/models/cost_model.py @@ -4,9 +4,9 @@ from typing import Literal, cast -CostModel = Literal['execution_time', 'quantum_compute_time'] +CostModel = Literal['2QGE_operations', 'execution_time', 'QCT', 'quantum_compute_time'] -COST_MODEL_VALUES: set[CostModel] = { 'execution_time', 'quantum_compute_time', } +COST_MODEL_VALUES: set[CostModel] = { '2QGE_operations', 'execution_time', 'QCT', 'quantum_compute_time', } def check_cost_model(value: str) -> CostModel: if value in COST_MODEL_VALUES: diff --git a/openapi-overlay.yaml b/openapi-overlay.yaml index d371a15..6ce154a 100644 --- a/openapi-overlay.yaml +++ b/openapi-overlay.yaml @@ -1,7 +1,7 @@ overlay: 1.0.0 info: title: ionq-core-python local OpenAPI fixes - version: 0.2.0 + version: 0.3.0 description: | Patches applied to openapi.json before client generation. The upstream spec marks QisCircuitInput.qubits as optional, but the simulator @@ -13,6 +13,9 @@ info: (and extended it to NativeCircuitInput and JsonMultiCircuitInput), so that action was removed. + CostModel is extended with live API values (``QCT``, ``2QGE_operations``) + that the platform returns but the vendored spec has not yet adopted. + actions: - target: $.components.schemas.QisCircuitInput.required remove: true @@ -22,3 +25,12 @@ actions: - circuit - gateset - qubits + - target: $.components.schemas.CostModel.enum + remove: true + - target: $.components.schemas.CostModel + update: + enum: + - quantum_compute_time + - execution_time + - QCT + - 2QGE_operations