|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +Instructions for AI agents working in this repository. Humans should read [`CONTRIBUTING.md`](CONTRIBUTING.md) first; this file restates the parts that are easy to violate. |
| 4 | + |
| 5 | +## What this is |
| 6 | + |
| 7 | +`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. |
| 8 | + |
| 9 | +## Setup |
| 10 | + |
| 11 | +```sh |
| 12 | +uv sync # canonical; uv.lock is committed and CI runs UV_FROZEN=true |
| 13 | +pre-commit install |
| 14 | +``` |
| 15 | + |
| 16 | +`uv` is required. Don't use `pip` / `poetry` for dev workflows — they bypass the lockfile. |
| 17 | + |
| 18 | +## Run |
| 19 | + |
| 20 | +```sh |
| 21 | +uv run pytest # unit tests; 100% branch coverage gate on hand-written code |
| 22 | +uv run ruff check |
| 23 | +uv run ruff format --check # drop --check to apply |
| 24 | +uv run ty check ionq_core/ |
| 25 | +uvx pre-commit run --all-files |
| 26 | + |
| 27 | +# Integration tests hit the real IonQ API. Deselected by default; weekly in CI. |
| 28 | +export IONQ_API_KEY=... |
| 29 | +uv run pytest -m integration --no-cov |
| 30 | +``` |
| 31 | + |
| 32 | +`pyproject.toml` is the source of truth for these invocations. Tests treat warnings as errors and use `xfail_strict=True`. |
| 33 | + |
| 34 | +## File boundary — the most important rule |
| 35 | + |
| 36 | +`ionq_core/` has two layers: |
| 37 | + |
| 38 | +- **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. |
| 39 | +- **Hand-written** — everything else under `ionq_core/`. Extend, fix bugs, add tests. |
| 40 | + |
| 41 | +To check whether a file is generated, look at `.gitattributes`: |
| 42 | + |
| 43 | +```sh |
| 44 | +grep -E '^ionq_core/' .gitattributes |
| 45 | +``` |
| 46 | + |
| 47 | +When you hit a bug in generated code: |
| 48 | +- **API surface** (endpoint, schema): upstream spec issue. File a bug; don't patch the file. |
| 49 | +- **Local schema fix** (e.g. tightening a type): add an action to `openapi-overlay.yaml` and regenerate. |
| 50 | +- **Generator-shape fix**: adjust `openapi-python-client-config.yaml` post-hooks or `custom-templates/`. |
| 51 | + |
| 52 | +## Regenerating the client |
| 53 | + |
| 54 | +Run exactly what's in [`CONTRIBUTING.md`](CONTRIBUTING.md) and mirrored in [`.github/workflows/generated.yml`](.github/workflows/generated.yml): |
| 55 | + |
| 56 | +```sh |
| 57 | +uv sync --group regen |
| 58 | +# If v0.4 isn't found, search for the latest API version. |
| 59 | +curl -sf https://api.ionq.co/v0.4/api-docs -o openapi.json |
| 60 | +uv run oas-patch overlay openapi.json openapi-overlay.yaml -o /tmp/patched-spec.json |
| 61 | +uv run openapi-python-client generate \ |
| 62 | + --path /tmp/patched-spec.json --meta none \ |
| 63 | + --config openapi-python-client-config.yaml \ |
| 64 | + --custom-template-path custom-templates \ |
| 65 | + --output-path ionq_core --overwrite |
| 66 | +``` |
| 67 | + |
| 68 | +Commit regenerated files in the same PR as the spec/template/overlay change that produced them. |
| 69 | + |
| 70 | +## Calling generated endpoints |
| 71 | + |
| 72 | +Every endpoint module exposes four callables: `sync`, `sync_detailed`, `asyncio`, `asyncio_detailed`. The `_detailed` variants return `Response[T]` (status + headers + parsed); the others return only the parsed body, or `None` on undocumented status when `raise_on_unexpected_status=False`. |
| 73 | + |
| 74 | +**Path params come first (positional or keyword). `client=`, `body=`, and all query params are keyword-only.** |
| 75 | + |
| 76 | +```python |
| 77 | +from ionq_core import IonQClient |
| 78 | +from ionq_core.api.default import create_job, get_job, get_compiled_file, get_jobs |
| 79 | +from ionq_core.models.circuit_job_creation_payload import CircuitJobCreationPayload |
| 80 | + |
| 81 | +client = IonQClient() # reads IONQ_API_KEY |
| 82 | +get_job.sync(uuid, client=client) # one path param |
| 83 | +get_compiled_file.sync(uuid, lang, client=client) # multiple path params |
| 84 | +get_jobs.sync(client=client, status="completed", limit=10) # query only |
| 85 | +create_job.sync(client=client, body=payload) # body only |
| 86 | +``` |
| 87 | + |
| 88 | +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. |
| 89 | + |
| 90 | +`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. |
| 91 | + |
| 92 | +Auth is `apiKey`, **not** `Bearer`. `IonQClient` sets `prefix="apiKey"`; the wire header is `Authorization: apiKey {token}`. Don't change this. |
| 93 | + |
| 94 | +## Hand-written conventions |
| 95 | + |
| 96 | +- 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). |
| 97 | +- Public API in each hand-written module is declared via `__all__` at the top; `ionq_core/__init__.py` re-exports those. |
| 98 | +- Type-checked by `ty` against Python 3.11. Ruff: `target-version = "py311"`, `line-length = 120`, `select = E, F, I, UP, B, SIM, RUF`. |
| 99 | +- 100% branch coverage on hand-written code (`--cov-fail-under=100`); generated paths are in `coverage.run.omit`. New conditional branches need new tests. |
| 100 | +- 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. |
| 101 | +- Mock HTTP with `httpx_mock` from `pytest-httpx`. Don't introduce `responses`, `requests-mock`, or VCR. |
| 102 | +- 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. |
| 103 | +- `gates.py` is intentionally NumPy-free (`cmath`, `math`, nested tuples). Keep it that way. |
| 104 | + |
| 105 | +## Drift sentinels — single edits that fan out |
| 106 | + |
| 107 | +Several values are pinned in multiple files (Python floor, generator/overlay version pins, 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 alignments — when 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. |
| 108 | + |
| 109 | +## CI |
| 110 | + |
| 111 | +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: |
| 112 | + |
| 113 | +- **`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. |
| 114 | +- **`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. |
| 115 | +- **`spec-drift.yml`** opens or updates a `spec-drift`-labeled issue when upstream `openapi.json` diverges from the vendored copy. |
| 116 | +- **`release.yml`** triggers on `v*` tags only and refuses mismatched tag/version pairs or republishing existing PyPI versions. |
| 117 | + |
| 118 | +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. |
| 119 | + |
| 120 | +## PR and release conventions |
| 121 | + |
| 122 | +- Branch off `main`. CODEOWNERS is `@ionq/developer-tools`. |
| 123 | +- PR titles become release-notes lines (`gh release create --generate-notes`). Imperative mood, user-facing, no leading ticket number. |
| 124 | +- User-visible changes go under `## [Unreleased]` in `CHANGELOG.md`, in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. |
| 125 | +- Release: bump `pyproject.toml` `[project] version`, promote `[Unreleased]` → `[X.Y.Z]` in `CHANGELOG.md`, tag `vX.Y.Z`. `release.yml` rejects mismatched tag/version pairs and refuses to republish an existing PyPI version. |
| 126 | + |
| 127 | +## Things to avoid (and what to do instead) |
| 128 | + |
| 129 | +- **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. |
| 130 | +- **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. |
| 131 | +- **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. |
| 132 | +- **`Bearer` token examples / `requests` / `aiohttp`** in docs or tests → the library is `httpx`-only and the auth prefix is `apiKey`. |
| 133 | +- **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. |
| 134 | +- **Lowering the Python floor in one file** → run the local checks in the "Run" section; `tests/test_docs_consistency.py` will list every peer that needs updating in the same commit. |
| 135 | +- **Adding NumPy or any new runtime dependency** to `gates.py` → keep it pure-Python. |
| 136 | + |
| 137 | +## Where to look first |
| 138 | + |
| 139 | +- Quick start: [`README.md`](README.md) (Bell-state on the simulator). |
| 140 | +- Endpoint inventory: `python -c "import json,sys; s=json.load(open('openapi.json')); [print(m.upper(), p) for p,ms in s['paths'].items() for m in ms if m in 'get post put delete patch']"` |
| 141 | +- Hand-written entry points: [`ionq_core/ionq_client.py`](ionq_core/ionq_client.py) (the `IonQClient` factory) and [`ionq_core/extensions.py`](ionq_core/extensions.py) (downstream-SDK API). |
| 142 | +- Drift checks: [`tests/test_docs_consistency.py`](tests/test_docs_consistency.py). |
| 143 | +- Integration smoke test (full job lifecycle): [`tests/integration/test_simulator_job.py`](tests/integration/test_simulator_job.py). |
0 commit comments