diff --git a/README.md b/README.md index 992bbde..3ed84f9 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ 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 + +See [`examples/`](examples/) for direct API examples, including sync and async downstream-SDK integration with the extension API. + ## 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..b1a70da --- /dev/null +++ b/examples/README.md @@ -0,0 +1,42 @@ +# Examples + +These examples show direct `ionq-core` usage close to the IonQ REST API. + +## Downstream SDK Integration + +`downstream_integration.py` and `downstream_integration_async.py` demonstrate +how a higher-level SDK can wrap `ionq-core` with the extension API while still +using the generated endpoint modules for job submission and result retrieval. + +The examples configure: + +- a downstream SDK `User-Agent` token +- SDK-specific default headers +- sync or async HTTP event hooks for request, response, and error logging +- an error mapper that wraps `APIError` and `RateLimitError` into an + SDK-defined exception type + +Install and run the sync example: + +```sh +pip install ionq-core +export IONQ_API_KEY=your-api-key +python examples/downstream_integration.py +``` + +Run the async example: + +```sh +python examples/downstream_integration_async.py +``` + +On Windows PowerShell, set the API key with: + +```powershell +$env:IONQ_API_KEY = "your-api-key" +python examples/downstream_integration.py +python examples/downstream_integration_async.py +``` + +Both scripts submit a Bell-state circuit to the free `simulator`, wait for the +job to complete, and print the returned probabilities. diff --git a/examples/downstream_integration.py b/examples/downstream_integration.py new file mode 100644 index 0000000..7d1b28e --- /dev/null +++ b/examples/downstream_integration.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: 2026 IonQ, Inc. +# SPDX-License-Identifier: Apache-2.0 + +"""Synchronous downstream-SDK integration example. + +This script demonstrates how a downstream SDK can use the ionq-core extension +API to add SDK-specific headers, a User-Agent token, sync HTTP event logging, +and error mapping while submitting a Bell-state circuit to the simulator. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import httpx + +from ionq_core import ( + APIError, + AuthenticatedClient, + 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 + + +class ExampleSDKError(Exception): + """Exception type that a downstream SDK might expose to its users.""" + + +class LoggingHook(EventHook): + """Minimal sync event hook used by the example SDK.""" + + def on_request(self, request: httpx.Request) -> None: + print(f"example-sdk -> {request.method} {request.url}") + + def on_response(self, request: httpx.Request, response: httpx.Response) -> None: + print(f"example-sdk <- {response.status_code} {request.method} {request.url.path}") + + def on_error(self, request: httpx.Request, error: Exception) -> None: + print(f"example-sdk !! {request.method} {request.url.path}: {error}") + + +def map_ionq_error(error: Exception) -> Exception: + """Translate ionq-core API errors into a downstream SDK exception.""" + if isinstance(error, RateLimitError): + return ExampleSDKError(f"IonQ rate limit exceeded; retry after {error.retry_after!r} seconds") + if isinstance(error, APIError): + return ExampleSDKError(f"IonQ API request failed: {error.message}") + return error + + +def example_sdk_client() -> AuthenticatedClient: + """Build an IonQ client configured as a downstream SDK would configure it.""" + return IonQClient( + extension=ClientExtension( + user_agent_token="example-sdk/0.1", + default_headers={"X-Example-SDK": "sync"}, + event_hooks=(LoggingHook(),), + error_mapper=map_ionq_error, + ) + ) + + +def bell_state_payload() -> CircuitJobCreationPayload: + """Build the Bell-state job payload used by the example.""" + return 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}, + ], + }, + } + ) + + +def run_bell_state(client: AuthenticatedClient) -> Mapping[str, float]: + """Submit a Bell-state job, wait for completion, and fetch probabilities.""" + created = create_job.sync(client=client, body=bell_state_payload()) + if created is None: + raise ExampleSDKError("IonQ API did not return a job creation response") + + completed = wait_for_job(client, created.id) + probabilities = get_job_probabilities.sync(uuid=completed.id, client=client) + if probabilities is None: + raise ExampleSDKError("IonQ API did not return probabilities") + return probabilities.additional_properties + + +def main() -> None: + """Run the sync downstream integration example.""" + client = example_sdk_client() + try: + probabilities: Mapping[str, Any] = run_bell_state(client) + print(dict(probabilities)) + finally: + client.get_httpx_client().close() + + +if __name__ == "__main__": + main() diff --git a/examples/downstream_integration_async.py b/examples/downstream_integration_async.py new file mode 100644 index 0000000..0882ded --- /dev/null +++ b/examples/downstream_integration_async.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2026 IonQ, Inc. +# SPDX-License-Identifier: Apache-2.0 + +"""Asynchronous downstream-SDK integration example. + +This script demonstrates how a downstream SDK can use the ionq-core extension +API to add SDK-specific headers, a User-Agent token, async HTTP event logging, +and error mapping while submitting a Bell-state circuit to the simulator. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +import httpx + +from ionq_core import ( + APIError, + AsyncEventHook, + AuthenticatedClient, + 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 + + +class ExampleAsyncSDKError(Exception): + """Exception type that an async downstream SDK might expose to its users.""" + + +class AsyncLoggingHook(AsyncEventHook): + """Minimal async event hook used by the example SDK.""" + + async def on_request(self, request: httpx.Request) -> None: + print(f"example-async-sdk -> {request.method} {request.url}") + + async def on_response(self, request: httpx.Request, response: httpx.Response) -> None: + print(f"example-async-sdk <- {response.status_code} {request.method} {request.url.path}") + + async def on_error(self, request: httpx.Request, error: Exception) -> None: + print(f"example-async-sdk !! {request.method} {request.url.path}: {error}") + + +def map_ionq_error(error: Exception) -> Exception: + """Translate ionq-core API errors into a downstream async SDK exception.""" + if isinstance(error, RateLimitError): + return ExampleAsyncSDKError(f"IonQ rate limit exceeded; retry after {error.retry_after!r} seconds") + if isinstance(error, APIError): + return ExampleAsyncSDKError(f"IonQ API request failed: {error.message}") + return error + + +def example_sdk_client() -> AuthenticatedClient: + """Build an IonQ client configured as an async downstream SDK.""" + return IonQClient( + extension=ClientExtension( + user_agent_token="example-async-sdk/0.1", + default_headers={"X-Example-SDK": "async"}, + async_event_hooks=(AsyncLoggingHook(),), + error_mapper=map_ionq_error, + ) + ) + + +def bell_state_payload() -> CircuitJobCreationPayload: + """Build the Bell-state job payload used by the example.""" + return 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}, + ], + }, + } + ) + + +async def run_bell_state(client: AuthenticatedClient) -> Mapping[str, float]: + """Submit a Bell-state job, wait for completion, and fetch probabilities.""" + created = await create_job.asyncio(client=client, body=bell_state_payload()) + if created is None: + raise ExampleAsyncSDKError("IonQ API did not return a job creation response") + + completed = await async_wait_for_job(client, created.id) + probabilities = await get_job_probabilities.asyncio(uuid=completed.id, client=client) + if probabilities is None: + raise ExampleAsyncSDKError("IonQ API did not return probabilities") + return probabilities.additional_properties + + +async def amain() -> None: + """Run the async downstream integration example.""" + client = example_sdk_client() + try: + probabilities: Mapping[str, Any] = await run_bell_state(client) + print(dict(probabilities)) + finally: + await client.get_async_httpx_client().aclose() + + +if __name__ == "__main__": + asyncio.run(amain())