Skip to content

Commit f4aa5f5

Browse files
committed
Code review improvements: bug fixes, test coverage, simplifications
1 parent 2805f36 commit f4aa5f5

13 files changed

Lines changed: 140 additions & 54 deletions

File tree

.github/workflows/release.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,26 @@ on:
77
permissions:
88
contents: read
99

10+
env:
11+
UV_FROZEN: true
12+
1013
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 10
17+
steps:
18+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
19+
with:
20+
persist-credentials: false
21+
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
22+
with:
23+
python-version: "3.12"
24+
enable-cache: false
25+
- run: uv sync
26+
- run: uv run pytest
27+
1128
build:
29+
needs: test
1230
runs-on: ubuntu-latest
1331
timeout-minutes: 10
1432
steps:

.gitignore

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
__pycache__/
22
*.pyc
3-
.venv/
4-
dist/
53
*.egg-info/
4+
dist/
5+
build/
6+
.venv/
67
.ruff_cache/
8+
.pytest_cache/
9+
.mypy_cache/
10+
htmlcov/
11+
.coverage
12+
docs/
13+
.DS_Store
14+
.env
15+
.idea/
16+
*.orig

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Most of the code in `ionq_core/` is **auto-generated** from the IonQ OpenAPI spe
3232
- `ionq_core/__init__.py` -- public API exports
3333
- `ionq_core/ionq_client.py` -- IonQClient convenience wrapper
3434
- `ionq_core/_exceptions.py` -- exception hierarchy
35+
- `ionq_core/_extensions.py` -- extension API for downstream SDKs
3536
- `ionq_core/_transport.py` -- retry transport
3637
- `ionq_core/_pagination.py` -- pagination helpers
3738
- `ionq_core/_polling.py` -- job polling helpers
@@ -48,7 +49,7 @@ else
4849
cp openapi.json /tmp/patched-spec.json
4950
fi
5051

51-
uvx openapi-python-client generate \
52+
uvx openapi-python-client==0.28.3 generate \
5253
--path /tmp/patched-spec.json \
5354
--meta none \
5455
--config openapi-python-client-config.yaml \

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ All exceptions inherit from `APIError`, which inherits from `IonQError`. Connect
141141

142142
## Retries
143143

144-
The client automatically retries transient errors (429, 500, 502, 503) and connection/timeout failures with exponential backoff. Default: 2 retries.
144+
The client automatically retries transient errors (429, 500, 502, 503, 520-529) and connection/timeout failures with exponential backoff. Default: 2 retries.
145145

146146
```python
147147
client = IonQClient(max_retries=5) # more retries
@@ -332,7 +332,7 @@ else
332332
fi
333333

334334
# Regenerate (preserves ionq_client.py)
335-
uvx openapi-python-client generate \
335+
uvx openapi-python-client==0.28.3 generate \
336336
--path /tmp/patched-spec.json \
337337
--meta none \
338338
--config openapi-python-client-config.yaml \
@@ -347,11 +347,11 @@ If the upstream spec contains patterns that the code generator cannot handle, fi
347347
## Development
348348

349349
```sh
350-
uv sync # Install dependencies
351-
uv run pytest # Run tests
352-
uv run ruff check ionq_core/ tests/ # Lint
353-
uv run ruff format ionq_core/ tests/ # Format
354-
uv run ty check ionq_core/ # Type check
350+
uv sync # Install dependencies
351+
uv run pytest # Run tests
352+
uv run ruff check # Lint
353+
uv run ruff format --check # Check formatting
354+
uv run ty check ionq_core/ # Type check
355355
```
356356

357357
## License

ionq_core/_exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Structured exceptions for the IonQ API client."""
22

3+
from __future__ import annotations
4+
35

46
class IonQError(Exception):
57
"""Base exception for all IonQ API errors."""

ionq_core/_transport.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Retry transport for httpx with exponential backoff."""
22

33
import asyncio
4+
import calendar
45
import email.utils
56
import logging
67
import random
78
import time
8-
from collections.abc import Generator
9+
from collections.abc import Iterator
910
from typing import NoReturn
1011

1112
import httpx
@@ -34,11 +35,11 @@ def _parse_retry_after(response: httpx.Response) -> float | None:
3435
pass
3536
parsed = email.utils.parsedate(header)
3637
if parsed is not None:
37-
return max(0.0, time.mktime(parsed) - time.time())
38+
return max(0.0, calendar.timegm(parsed) - time.time())
3839
return None
3940

4041

41-
def _backoff_delays(max_retries: int) -> Generator[float]:
42+
def _backoff_delays(max_retries: int) -> Iterator[float]:
4243
yield 0.0
4344
for attempt in range(max_retries):
4445
base = 0.5 * (2**attempt)

ionq_core/ionq_client.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@ def _build_user_agent(*tokens: str | None) -> str:
3232
f"python/{platform.python_version()}",
3333
f"httpx/{httpx.__version__}",
3434
f"os/{platform.system().lower()}",
35+
*filter(None, tokens),
3536
]
36-
for token in tokens:
37-
if token:
38-
parts.append(token)
3937
return " ".join(parts)
4038

4139

@@ -107,9 +105,7 @@ def IonQClient(
107105
effective_timeout = (
108106
extension.timeout if (extension and extension.timeout is not None) else (timeout or _DEFAULT_TIMEOUT)
109107
)
110-
effective_retries = (
111-
extension.max_retries if (extension and extension.max_retries is not None) else max_retries
112-
)
108+
effective_retries = extension.max_retries if (extension and extension.max_retries is not None) else max_retries
113109
effective_retry_codes = (
114110
extension.retryable_status_codes
115111
if (extension and extension.retryable_status_codes is not None)

pyproject.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ authors = [{name = "IonQ", email = "support@ionq.co"}]
1010
keywords = ["ionq", "quantum", "quantum-computing", "sdk", "api-client"]
1111
classifiers = [
1212
"Development Status :: 3 - Alpha",
13+
"Intended Audience :: Developers",
1314
"License :: OSI Approved :: Apache Software License",
15+
"Operating System :: OS Independent",
1416
"Programming Language :: Python :: 3",
1517
"Programming Language :: Python :: 3.12",
1618
"Programming Language :: Python :: 3.13",
@@ -49,9 +51,8 @@ build-backend = "hatchling.build"
4951
packages = ["ionq_core"]
5052

5153
[tool.ruff]
52-
line-length = 120
5354
target-version = "py312"
54-
preview = true
55+
line-length = 120
5556
extend-exclude = [
5657
"ionq_core/api",
5758
"ionq_core/models",
@@ -64,11 +65,14 @@ extend-exclude = [
6465
docstring-code-format = true
6566

6667
[tool.ruff.lint]
67-
select = ["E", "F", "I", "UP", "B", "SIM"]
68+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
6869

6970
[tool.ruff.lint.isort]
7071
known-first-party = ["ionq_core"]
7172

73+
[tool.ruff.lint.per-file-ignores]
74+
"tests/**" = ["RUF012"]
75+
7276
[tool.ty.environment]
7377
python-version = "3.12"
7478

@@ -80,7 +84,9 @@ invalid-argument-type = "ignore"
8084
[tool.pytest.ini_options]
8185
testpaths = ["tests"]
8286
asyncio_mode = "auto"
87+
xfail_strict = true
88+
filterwarnings = ["error"]
8389
markers = [
8490
"integration: marks tests that hit the real IonQ API (deselect with '-m \"not integration\"')",
8591
]
86-
addopts = "-m 'not integration'"
92+
addopts = "-m 'not integration' --tb=short"

tests/integration/conftest.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55

66
import pytest
77

8-
from ionq_core import IonQClient
8+
from ionq_core import AuthenticatedClient, IonQClient
99
from ionq_core.api.default import delete_job
10-
from ionq_core.client import AuthenticatedClient
11-
12-
pytestmark = pytest.mark.integration
1310

1411
_job_ids: list[str] = []
1512

tests/test_api.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,17 @@ def test_sync(self, httpx_mock, auth_client):
110110
status_code=201,
111111
json={"id": "new-job-id", "status": "submitted", "session_id": None},
112112
)
113-
body = CircuitJobCreationPayload.from_dict({
114-
"type": "ionq.circuit.v1",
115-
"backend": "simulator",
116-
"shots": 100,
117-
"input": {
118-
"gateset": "qis",
119-
"circuit": [{"gate": "h", "targets": [0]}],
120-
},
121-
})
113+
body = CircuitJobCreationPayload.from_dict(
114+
{
115+
"type": "ionq.circuit.v1",
116+
"backend": "simulator",
117+
"shots": 100,
118+
"input": {
119+
"gateset": "qis",
120+
"circuit": [{"gate": "h", "targets": [0]}],
121+
},
122+
}
123+
)
122124
result = create_job.sync(client=auth_client, body=body)
123125
assert isinstance(result, JobCreationResponse)
124126
assert result.id == "new-job-id"

0 commit comments

Comments
 (0)