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
3 changes: 1 addition & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Auto-generated by openapi-python-client.
# Generated by openapi-python-client.
ionq_core/__init__.py linguist-generated=true
ionq_core/client.py linguist-generated=true
ionq_core/errors.py linguist-generated=true
Expand All @@ -9,5 +9,4 @@ ionq_core/models/** linguist-generated=true
# Vendored upstream OpenAPI spec.
openapi.json linguist-generated=true

# Lockfile.
uv.lock linguist-generated=true
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# Default owners for everything in the repo.
* @ionq/developer-tools
10 changes: 5 additions & 5 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug report. Please search [existing issues](https://github.com/ionq/ionq-core-python/issues) first.
Search [existing issues](https://github.com/ionq/ionq-core-python/issues) before filing.

- type: dropdown
id: area
attributes:
label: Affected area
description: |
See [proposing changes](https://github.com/ionq/ionq-core-python/blob/main/CONTRIBUTING.md#proposing-changes) for the boundary between generated and hand-written code.
See [proposing changes](https://github.com/ionq/ionq-core-python/blob/main/CONTRIBUTING.md#proposing-changes) for the generated vs hand-written boundary.
options:
- Generated client (regenerated from OpenAPI spec)
- Hand-written extensions (retry, pagination, polling, sessions, native gates, etc.)
Expand All @@ -28,21 +28,21 @@ body:
id: what-happened
attributes:
label: What happened?
description: A clear description of the bug, including any error message or traceback.
description: Include any error message or traceback.
validations:
required: true

- type: textarea
id: expected
attributes:
label: What did you expect to happen?
description: Optional - skip if a traceback or error message above already shows the problem.
description: Optional - skip if the error above already shows the problem.

- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Minimal code or steps to reproduce the bug.
description: Minimal code or steps to reproduce.
render: Python
validations:
required: true
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ contact_links:
about: Email security@ionq.co. Do not open a public issue.
- name: IonQ Support
url: https://ionq.com/contact
about: For account, billing, or platform questions, contact IonQ support directly.
about: Account, billing, or platform questions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
Please search [existing issues](https://github.com/ionq/ionq-core-python/issues) before opening a new request. For API surface changes (new endpoints, parameter names, response shapes), see [proposing changes](https://github.com/ionq/ionq-core-python/blob/main/CONTRIBUTING.md#proposing-changes).
Search [existing issues](https://github.com/ionq/ionq-core-python/issues) first. For API surface changes (endpoints, parameters, response shapes), see [proposing changes](https://github.com/ionq/ionq-core-python/blob/main/CONTRIBUTING.md#proposing-changes).

- type: textarea
id: description
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/spec-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
# Pinned (tests/test_docs_consistency.py keeps it aligned with DEFAULT_BASE_URL);
# never derived from openapi.json, so a tampered vendored spec cannot point the
# check at a mirror that hides it.
# Pinned, never derived from openapi.json, so a tampered spec cannot redirect this
# check. tests/test_docs_consistency.py keeps it aligned with DEFAULT_BASE_URL.
SPEC_URL: https://api.ionq.co/v0.4/api-docs
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
44 changes: 22 additions & 22 deletions AGENTS.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth a comment in this file about catching drift between docs and code, and also whatever writing style you recommended for the agent that made the changes in this PR.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Instructions for AI agents working in this repository. Humans should read [`CONT

## What this is

`ionq-core` is a typed, sync+async Python REST client for the [IonQ Cloud Platform API](https://api.ionq.co/v0.4). Most of `ionq_core/` is **generated** from `openapi.json` via `openapi-python-client`; a small **hand-written** layer at the package root adds retries, hooks, pagination, polling, sessions, structured exceptions, and native-gate unitaries. Apache-2.0, published to PyPI as `ionq-core` (see `pyproject.toml` `[project] version` and `classifiers` for current release status). Most end users should pick a higher-level wrapper (`qiskit-ionq`, `cirq-ionq`, `pennylane-ionq`, CUDA-Q, qbraid) `ionq-core` is the wire-level building block those SDKs sit on.
`ionq-core` is a typed, sync+async Python REST client for the [IonQ Cloud Platform API](https://api.ionq.co/v0.4). Most of `ionq_core/` is **generated** from `openapi.json` via `openapi-python-client`; a small **hand-written** layer at the package root adds retries, hooks, pagination, polling, sessions, structured exceptions, and native-gate unitaries. Apache-2.0, on PyPI as `ionq-core` (see `pyproject.toml` `[project] version` and `classifiers` for release status). Most end users want a higher-level wrapper (`qiskit-ionq`, `cirq-ionq`, `pennylane-ionq`, CUDA-Q, qbraid); `ionq-core` is the wire-level building block those SDKs sit on.

## Setup

Expand All @@ -13,7 +13,7 @@ uv sync # canonical; uv.lock is committed and CI runs UV_FROZEN=t
uvx pre-commit install
```

`uv` is required. Don't use `pip` / `poetry` for dev workflows — they bypass the lockfile.
`uv` is required for dev workflows; `pip` / `poetry` bypass the lockfile.

## Run

Expand All @@ -31,14 +31,14 @@ uv run pytest -m integration --no-cov

`pyproject.toml` is the source of truth for these invocations. Tests treat warnings as errors and use `xfail_strict=True`.

## File boundary the most important rule
## File boundary - the most important rule

`ionq_core/` has two layers:

- **Generated** overwritten on every regeneration. The set is enumerated in [`.gitattributes`](.gitattributes) (`linguist-generated=true` lines) and mirrored in `pyproject.toml`'s `ruff.extend-exclude` + `coverage.run.omit`; `tests/test_docs_consistency.py` keeps the three lists aligned. The one exception is `ionq_core/__init__.py`, which is in `.gitattributes` only — its content is rendered from [`custom-templates/package_init.py.jinja`](custom-templates/package_init.py.jinja) but the rendered output is still linted and coverage-checked.
- **Hand-written** everything else under `ionq_core/`. Extend, fix bugs, add tests.
- **Generated** - overwritten on every regeneration. Listed in [`.gitattributes`](.gitattributes) (`linguist-generated=true` lines) and mirrored in `pyproject.toml`'s `ruff.extend-exclude` + `coverage.run.omit`; `tests/test_docs_consistency.py` keeps the three lists aligned. Exception: `ionq_core/__init__.py` is in `.gitattributes` only - it renders from [`custom-templates/package_init.py.jinja`](custom-templates/package_init.py.jinja), but the rendered output is still linted and coverage-checked.
- **Hand-written** - everything else under `ionq_core/`. Extend, fix bugs, add tests.

To check whether a file is generated, look at `.gitattributes`:
To check whether a file is generated:

```sh
grep -E '^ionq_core/' .gitattributes
Expand All @@ -51,7 +51,7 @@ When you hit a bug in generated code:

## Regenerating the client

Run the block in [`CONTRIBUTING.md`](CONTRIBUTING.md#regenerating-the-client) verbatim; CI runs the same invocation via [`generated.yml`](.github/workflows/generated.yml) on every PR. The spec source is `https://api.ionq.co/v0.4/api-docs` (if that version 404s, search for the current one). Commit regenerated files in the same PR as the spec/template/overlay change that produced them.
Run the block in [`CONTRIBUTING.md`](CONTRIBUTING.md#regenerating-the-client) verbatim; [`generated.yml`](.github/workflows/generated.yml) runs the same invocation on every PR. The spec source is `https://api.ionq.co/v0.4/api-docs` (if that version 404s, find the current one). Commit regenerated files in the same PR as the spec/template/overlay change that produced them.

## Calling generated endpoints

Expand All @@ -71,37 +71,37 @@ get_jobs.sync(client=client, status="completed", limit=10) # query only
create_job.sync(client=client, body=payload) # body only
```

Use `next_=` (trailing underscore) for the cursor pagination kwarg Python keyword collision. The `iter_jobs` / `aiter_jobs` / `iter_session_jobs` / `aiter_session_jobs` helpers handle paging for you.
Use `next_=` (trailing underscore) for the cursor pagination kwarg - Python keyword collision. `iter_jobs` / `aiter_jobs` / `iter_session_jobs` / `aiter_session_jobs` page for you.

`UNSET` (sentinel from `ionq_core.types`) means "field omitted"; `None` serializes as JSON `null`. `to_dict()` skips `UNSET` and emits `null` for `None`. Don't conflate.
`UNSET` (sentinel from `ionq_core.types`) means "field omitted"; `None` serializes as JSON `null`. `to_dict()` skips `UNSET` and emits `null` for `None`.

Auth is `apiKey`, **not** `Bearer`. `IonQClient` sets `prefix="apiKey"`; the wire header is `Authorization: apiKey {token}`. Don't change this.
Auth is `apiKey`, **not** `Bearer`: `IonQClient` sets `prefix="apiKey"` and the wire header is `Authorization: apiKey {token}`. Don't change this.

## Hand-written conventions

- Every `.py` carries an SPDX header (`# SPDX-FileCopyrightText: <year> IonQ, Inc.` + `Apache-2.0`); generated files also carry `# @generated`. The year must be **uniform across the whole package** `tests/test_docs_consistency.py` fails CI otherwise. At the year boundary, bump every hand-written file to match (the generator post-hook does the rest).
- Public API in each hand-written module is declared via `__all__` at the top; `ionq_core/__init__.py` re-exports those.
- Every `.py` carries an SPDX header (`# SPDX-FileCopyrightText: <year> IonQ, Inc.` + `Apache-2.0`); generated files also carry `# @generated`. The year must be **uniform across the whole package** or `tests/test_docs_consistency.py` fails CI. At the year boundary, bump every hand-written file to match; the generator post-hook does the rest.
- Each hand-written module declares its public API in `__all__` at the top; `ionq_core/__init__.py` re-exports those.
- Type-checked by `ty` against Python 3.11. Ruff: `target-version = "py311"`, `line-length = 120`, `select = E, F, I, UP, B, SIM, RUF`.
- 100% branch coverage on hand-written code (`--cov-fail-under=100`); generated paths are in `coverage.run.omit`. New conditional branches need new tests.
- Test fixtures live in [`tests/conftest.py`](tests/conftest.py): `client` (unauth) and `auth_client` (token `"test-api-key"`, `prefix="apiKey"`), both pointing at `https://test.invalid/v0.4`. Use them; don't construct clients ad hoc.
- Fixtures and shared helpers live in [`tests/conftest.py`](tests/conftest.py); its clients point at a `test.invalid` base URL derived from `DEFAULT_BASE_URL`. Use them instead of constructing clients ad hoc.
- Mock HTTP with `httpx_mock` from `pytest-httpx`. Don't introduce `responses`, `requests-mock`, or VCR.
- Integration tests are marked `pytest.mark.integration` and live in `tests/integration/`. Use the `track_job` fixture so the autouse `cleanup_jobs` fixture deletes anything you create.
- `gates.py` is intentionally NumPy-free (`cmath`, `math`, nested tuples). Keep it that way.

## Drift sentinels single edits that fan out
## Drift sentinels - single edits that fan out

Several values are pinned in multiple files (Python floor, API base URL, the generated-path set, numeric defaults that appear in both code and docstrings). [`tests/test_docs_consistency.py`](tests/test_docs_consistency.py) is the canonical list of these alignmentswhen it fails, read the failing assertion to find the peers and update every one in the same PR. Treat that test file as the source of truth; it grows as new pinned values are added.
Several values are pinned in multiple files (Python floor, API base URL, the generated-path set, numeric defaults that appear in both code and docstrings). [`tests/test_docs_consistency.py`](tests/test_docs_consistency.py) is the canonical, growing list of these alignments; when it fails, read the failing assertion to find the peers and update every one in the same PR.

## CI

Workflows live in [`.github/workflows/`](.github/workflows/) `ls` it for the current set; each file's `on:` block documents its own triggers. Four have non-obvious behavior worth knowing about:
Workflows live in [`.github/workflows/`](.github/workflows/) - `ls` it for the current set; each file's `on:` block documents its own triggers. Non-obvious behavior:

- **`generated.yml`** runs the regenerator on every PR and fails if `git diff ionq_core/` is non-empty. This is what catches hand-edits to generated files.
- **`integration.yml`** is on a weekly cron and `workflow_dispatch` only — it does not run per PR, so don't rely on it for fast feedback.
- **`generated.yml`** runs the regenerator on every PR and fails if `git diff ionq_core/` is non-empty. This catches hand-edits to generated files.
- **`integration.yml`** runs on a weekly cron and `workflow_dispatch` only, never per PR, so don't rely on it for fast feedback.
- **`spec-drift.yml`** opens or updates a `spec-drift`-labeled issue when upstream `openapi.json` diverges from the vendored copy.
- **`release.yml`** triggers on `v*` tags only and refuses mismatched tag/version pairs or republishing existing PyPI versions.

When authoring a new workflow, use the local [`.github/actions/setup-uv`](.github/actions/setup-uv) composite action rather than `astral-sh/setup-uv` directly, for consistency with the existing matrix.
New workflows must use the local [`.github/actions/setup-uv`](.github/actions/setup-uv) composite action, not `astral-sh/setup-uv` directly, for consistency with the existing matrix.

## PR and release conventions

Expand All @@ -112,11 +112,11 @@ When authoring a new workflow, use the local [`.github/actions/setup-uv`](.githu

## Things to avoid (and what to do instead)

- **Including IonQ confidential information** in any committed artifact code, comments, commit messages, branch names, PR titles/bodies, test fixtures, or docstrings → scrub before pushing; the repo is public (Apache-2.0 on PyPI) and a leak can't be cleanly undone. Confidential covers proprietary algorithms, trade secrets, internal project codenames, internal file paths, server names, IP addresses, API keys, passwords, non-public experimental data, sensitive customer information, PII, and internal-only comments or documentation.
- **Including IonQ confidential information** in any committed artifact - code, comments, commit messages, branch names, PR titles/bodies, test fixtures, docstrings → scrub before pushing; the repo is public (Apache-2.0 on PyPI) and a leak can't be cleanly undone. Confidential covers proprietary algorithms, trade secrets, internal project codenames, internal file paths, server names, IP addresses, API keys, passwords, non-public experimental data, sensitive customer information, PII, and internal-only comments or documentation.
- **Editing generated files by hand** → fix the spec, the overlay, the post-hooks, or the template, then regenerate. CI's `generated.yml` will catch it otherwise.
- **Adding a dependency with `pip install`** → `uv add <pkg>` (or edit `pyproject.toml` and `uv lock`). Confirm the dependency's license before adding: MIT, Apache-2.0, BSD-2-Clause, and BSD-3-Clause are pre-approved.
- **Adding a dependency with `pip install`** → `uv add <pkg>` (or edit `pyproject.toml` and `uv lock`). Check its license first: MIT, Apache-2.0, BSD-2-Clause, and BSD-3-Clause are pre-approved.
- **`Bearer` token examples / `requests` / `aiohttp`** in docs or tests → the library is `httpx`-only and the auth prefix is `apiKey`.
- **Dropping the SPDX header or `# @generated` marker** on regenerated files → if a post-hook regression made this happen, fix `openapi-python-client-config.yaml` rather than re-adding by hand.
- **Dropping the SPDX header or `# @generated` marker** on regenerated files → a post-hook adds them, so fix `openapi-python-client-config.yaml` rather than re-adding by hand.
- **Adding NumPy or any new runtime dependency** to `gates.py` → keep it pure-Python.

## Where to look first
Expand Down
Loading
Loading