openquantum-sdk 0.4.2__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openquantum_sdk-1.0.0/MANIFEST.in +10 -0
- {openquantum_sdk-0.4.2/openquantum_sdk.egg-info → openquantum_sdk-1.0.0}/PKG-INFO +35 -8
- openquantum_sdk-0.4.2/PKG-INFO → openquantum_sdk-1.0.0/README.md +28 -27
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/__init__.py +18 -1
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/__main__.py +23 -33
- openquantum_sdk-1.0.0/openquantum_sdk/accounts.py +257 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/clients.py +269 -56
- openquantum_sdk-1.0.0/openquantum_sdk/credentials.py +104 -0
- openquantum_sdk-1.0.0/openquantum_sdk/exceptions.py +38 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/models.py +4 -2
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/qiskit/__init__.py +12 -7
- openquantum_sdk-1.0.0/openquantum_sdk/session.py +352 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/utils.py +26 -8
- openquantum_sdk-0.4.2/README.md → openquantum_sdk-1.0.0/openquantum_sdk.egg-info/PKG-INFO +54 -5
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk.egg-info/SOURCES.txt +7 -1
- openquantum_sdk-1.0.0/openquantum_sdk.egg-info/requires.txt +21 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk.egg-info/top_level.txt +1 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/pyproject.toml +6 -4
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/tests/test_backends.py +11 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/tests/test_clients_plan_priority.py +141 -1
- openquantum_sdk-1.0.0/tests/test_polling.py +84 -0
- openquantum_sdk-1.0.0/tests/test_qiskit_wrappers.py +207 -0
- openquantum_sdk-1.0.0/tests/test_session_auth.py +270 -0
- openquantum_sdk-0.4.2/MANIFEST.in +0 -4
- openquantum_sdk-0.4.2/openquantum_sdk.egg-info/requires.txt +0 -15
- openquantum_sdk-0.4.2/tests/test_qiskit_wrappers.py +0 -76
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/auth.py +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/backends.py +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk/enums.py +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk.egg-info/dependency_links.txt +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/openquantum_sdk.egg-info/entry_points.txt +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/requirements-dev.txt +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/requirements.txt +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/setup.cfg +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/tests/__init__.py +0 -0
- {openquantum_sdk-0.4.2 → openquantum_sdk-1.0.0}/tests/test_clients.py +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Unit tests ship in the sdist. packages.find excludes them from the wheel,
|
|
2
|
+
# so they are not installed into site-packages.
|
|
3
|
+
# plugins/ is a separate set of distributions and must not ride along.
|
|
4
|
+
# smoke/ is the live runner for the manual SDK smoke workflow.
|
|
5
|
+
include README.md
|
|
6
|
+
include requirements*.txt
|
|
7
|
+
recursive-include tests *.py
|
|
8
|
+
global-exclude *.pyc
|
|
9
|
+
prune plugins
|
|
10
|
+
prune smoke
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openquantum-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python SDK for Open Quantum
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -16,9 +16,13 @@ Requires-Dist: requests-mock>=1.10.0; extra == "dev"
|
|
|
16
16
|
Requires-Dist: flake8; extra == "dev"
|
|
17
17
|
Requires-Dist: black; extra == "dev"
|
|
18
18
|
Provides-Extra: qiskit
|
|
19
|
-
Requires-Dist: openquantum-sdk-qiskit>=0.
|
|
19
|
+
Requires-Dist: openquantum-sdk-qiskit>=1.0.0; extra == "qiskit"
|
|
20
20
|
Provides-Extra: pennylane
|
|
21
|
-
Requires-Dist: openquantum-sdk-pennylane>=0.
|
|
21
|
+
Requires-Dist: openquantum-sdk-pennylane>=1.0.0; extra == "pennylane"
|
|
22
|
+
Provides-Extra: cirq
|
|
23
|
+
Requires-Dist: openquantum-sdk-cirq>=0.5.0; extra == "cirq"
|
|
24
|
+
Provides-Extra: qrisp
|
|
25
|
+
Requires-Dist: openquantum-sdk-qrisp>=0.5.0; extra == "qrisp"
|
|
22
26
|
|
|
23
27
|
# Open Quantum Python SDK
|
|
24
28
|
|
|
@@ -44,6 +48,8 @@ pip install -e .
|
|
|
44
48
|
|
|
45
49
|
Python 3.8+ required.
|
|
46
50
|
|
|
51
|
+
Source distributions include the `tests/` tree so the unit tests can be run from an sdist. Wheels do not install those tests into `site-packages`. The Qiskit, PennyLane, Cirq, and Qrisp plugins, and `openquantum-mcp`, use the same split. Package versions live in each `pyproject.toml` and `__version__` (core, Qiskit, and PennyLane `1.0.0`; Cirq and Qrisp `0.5.0`; MCP `0.2.0`).
|
|
52
|
+
|
|
47
53
|
## Authentication
|
|
48
54
|
|
|
49
55
|
### Option 1: SDK Key File (Recommended)
|
|
@@ -78,6 +84,18 @@ export OPENQUANTUM_CLIENT_ID=s_...
|
|
|
78
84
|
export OPENQUANTUM_CLIENT_SECRET=e460c8...
|
|
79
85
|
```
|
|
80
86
|
|
|
87
|
+
### `OpenQuantumClient.from_env()`
|
|
88
|
+
|
|
89
|
+
Builds a client from those environment credentials and from `OPENQUANTUM_SCHEDULER_URL`, `OPENQUANTUM_MANAGEMENT_URL`, `OPENQUANTUM_KEYCLOAK_BASE`, and `OPENQUANTUM_KEYCLOAK_REALM`. It does not autoload a saved account. It raises `AuthenticationError` when no credential resolves.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from openquantum_sdk import OpenQuantumClient
|
|
93
|
+
|
|
94
|
+
client = OpenQuantumClient.from_env()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`OPENQUANTUM_ORGANIZATION_ID` is not read by the core client constructor. PennyLane, Cirq, Qrisp, `get_backend`, and the MCP server use it when `organization_id` is omitted. The old names `OQ_ORG_ID` and `OQ_SUBCATEGORY_ID` are not read.
|
|
98
|
+
|
|
81
99
|
## Quickstart: One-Call Submission (Recommended)
|
|
82
100
|
|
|
83
101
|
```python
|
|
@@ -282,9 +300,10 @@ finally:
|
|
|
282
300
|
| `name` | `str` | — | **Required** |
|
|
283
301
|
| `shots` | `int` | — | **Required** |
|
|
284
302
|
| `configuration_data` | `Dict[str, Any]` |`None` | |
|
|
285
|
-
| `execution_plan` | `
|
|
286
|
-
| `queue_priority` | `
|
|
303
|
+
| `execution_plan` | `str`, enum, or UUID | `"auto"` | `"auto"`, `"public"`, `"private"`, `ExecutionPlanType`, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
304
|
+
| `queue_priority` | `str`, enum, or UUID | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, `QueuePriorityType`, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
287
305
|
| `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
|
|
306
|
+
| `verbose` | `bool` | `False` | Print submission progress. Same default on every plugin |
|
|
288
307
|
| `job_timeout_seconds` | `int` | `86400` | 1 day |
|
|
289
308
|
| `input_format` | `str` | `"qasm"` | OpenQASM by default; other values only if listed in backend `supported_formats` |
|
|
290
309
|
| `submitted_with` | `str` | `"sdk"` | Client surface for analytics |
|
|
@@ -301,7 +320,10 @@ finally:
|
|
|
301
320
|
| `upload_job_input(file_path=... or file_content=...)` | `str` | Returns upload ID |
|
|
302
321
|
| `prepare_job(JobPreparationCreate)` | `JobPreparationResponse` | Starts preparation |
|
|
303
322
|
| `get_preparation_result(preparation_id)` | `JobPreparationResultResponse` | Includes `.quote` |
|
|
323
|
+
| `wait_for_preparation(preparation_id, timeout=300, interval=2)` | `JobPreparationResultResponse` | Poll until preparation completes |
|
|
324
|
+
| `choose_plan_and_priority(result, execution_plan="auto", queue_priority="auto")` | `PlanSelection` | Friendly names, enums, and UUIDs. `prefer_public=True` ranks Public first when the plan is `"auto"` |
|
|
304
325
|
| `create_job(JobCreate)` | `JobRead` | Submits job |
|
|
326
|
+
| `wait_for_job(job_id, timeout=300, interval=5)` | `JobRead` | Poll until a terminal status. `raise_on_timeout` and `raise_on_failure` default to `True` |
|
|
305
327
|
| `get_job(job_id)` | `JobRead` | Fetch job status |
|
|
306
328
|
| `cancel_job(job_id)` | `None` | Cancel running job |
|
|
307
329
|
| `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
|
|
@@ -353,8 +375,9 @@ from openquantum_sdk.utils import poll_for_status
|
|
|
353
375
|
| ------ | ------ | ------ | ------ |
|
|
354
376
|
| `get_status_fn` | `Callable[[str], Tuple[bool, Any]]` | | Function to be polled |
|
|
355
377
|
| `resource_id` | `str` | | id to be passed to status function: `get_status_fn(resource_id)` |
|
|
356
|
-
| `interval` | `
|
|
357
|
-
| `timeout` | `
|
|
378
|
+
| `interval` | `float` | `5` | Seconds to wait before the next status call |
|
|
379
|
+
| `timeout` | `float` | `300` | Seconds before polling stops |
|
|
380
|
+
| `raise_on_timeout` | `bool` | `True` | Raise `TimeoutError`, or return the last result when `False` |
|
|
358
381
|
|
|
359
382
|
|
|
360
383
|
## Enums
|
|
@@ -455,13 +478,17 @@ for cat in cats:
|
|
|
455
478
|
```
|
|
456
479
|
|
|
457
480
|
|
|
481
|
+
## Live smoke tests
|
|
482
|
+
|
|
483
|
+
`.github/workflows/sdk-smoke.yml` is a manual Actions workflow. It builds the core SDK and the Qiskit, PennyLane, Cirq, and Qrisp plugins from a git ref (empty means `main`), submits a Bell circuit through each one, and waits until the platform job completes. The QPU input defaults to `rigetti:cepheus-1-108q`. Choose the `dev` or `prd` environment when you run it. See [sdk/smoke/README.md](smoke/README.md).
|
|
484
|
+
|
|
458
485
|
## FAQ
|
|
459
486
|
|
|
460
487
|
**Q: How do I get results as JSON?**
|
|
461
488
|
**A:** Use `scheduler.download_job_output(job)` — returns parsed object.
|
|
462
489
|
|
|
463
490
|
**Q: Why does auto mode fail on some QPUs?**
|
|
464
|
-
**A:** `"auto"` selects the cheapest plan and priority (`
|
|
491
|
+
**A:** `"auto"` selects the cheapest plan and priority, which is often Public + Standard. It is not limited to those two. Friendly names (`"public"`, `"private"`, `"standard"`, `"priority"`, `"instant"`), enums, and UUIDs select that option exactly. If you need a specific plan or a faster queue, pass it explicitly. The MCP server passes `prefer_public=True` so Spark credits can apply; `submit_job()` does not.
|
|
465
492
|
|
|
466
493
|
**Q: Can I hide the public plan citation notice?**
|
|
467
494
|
**A:** Setting `OPENQUANTUM_HIDE_CITATION_NOTICE=true` will omit the citation notice from output.
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: openquantum-sdk
|
|
3
|
-
Version: 0.4.2
|
|
4
|
-
Summary: Python SDK for Open Quantum
|
|
5
|
-
Classifier: Programming Language :: Python :: 3
|
|
6
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
7
|
-
Classifier: Operating System :: OS Independent
|
|
8
|
-
Requires-Python: >=3.8
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: requests>=2.25.0
|
|
11
|
-
Provides-Extra: dev
|
|
12
|
-
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
14
|
-
Requires-Dist: pytest-mock; extra == "dev"
|
|
15
|
-
Requires-Dist: requests-mock>=1.10.0; extra == "dev"
|
|
16
|
-
Requires-Dist: flake8; extra == "dev"
|
|
17
|
-
Requires-Dist: black; extra == "dev"
|
|
18
|
-
Provides-Extra: qiskit
|
|
19
|
-
Requires-Dist: openquantum-sdk-qiskit>=0.3.1; extra == "qiskit"
|
|
20
|
-
Provides-Extra: pennylane
|
|
21
|
-
Requires-Dist: openquantum-sdk-pennylane>=0.2.1; extra == "pennylane"
|
|
22
|
-
|
|
23
1
|
# Open Quantum Python SDK
|
|
24
2
|
|
|
25
3
|
A lightweight, modern Python SDK for the **Open Quantum Platform**.
|
|
@@ -44,6 +22,8 @@ pip install -e .
|
|
|
44
22
|
|
|
45
23
|
Python 3.8+ required.
|
|
46
24
|
|
|
25
|
+
Source distributions include the `tests/` tree so the unit tests can be run from an sdist. Wheels do not install those tests into `site-packages`. The Qiskit, PennyLane, Cirq, and Qrisp plugins, and `openquantum-mcp`, use the same split. Package versions live in each `pyproject.toml` and `__version__` (core, Qiskit, and PennyLane `1.0.0`; Cirq and Qrisp `0.5.0`; MCP `0.2.0`).
|
|
26
|
+
|
|
47
27
|
## Authentication
|
|
48
28
|
|
|
49
29
|
### Option 1: SDK Key File (Recommended)
|
|
@@ -78,6 +58,18 @@ export OPENQUANTUM_CLIENT_ID=s_...
|
|
|
78
58
|
export OPENQUANTUM_CLIENT_SECRET=e460c8...
|
|
79
59
|
```
|
|
80
60
|
|
|
61
|
+
### `OpenQuantumClient.from_env()`
|
|
62
|
+
|
|
63
|
+
Builds a client from those environment credentials and from `OPENQUANTUM_SCHEDULER_URL`, `OPENQUANTUM_MANAGEMENT_URL`, `OPENQUANTUM_KEYCLOAK_BASE`, and `OPENQUANTUM_KEYCLOAK_REALM`. It does not autoload a saved account. It raises `AuthenticationError` when no credential resolves.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from openquantum_sdk import OpenQuantumClient
|
|
67
|
+
|
|
68
|
+
client = OpenQuantumClient.from_env()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`OPENQUANTUM_ORGANIZATION_ID` is not read by the core client constructor. PennyLane, Cirq, Qrisp, `get_backend`, and the MCP server use it when `organization_id` is omitted. The old names `OQ_ORG_ID` and `OQ_SUBCATEGORY_ID` are not read.
|
|
72
|
+
|
|
81
73
|
## Quickstart: One-Call Submission (Recommended)
|
|
82
74
|
|
|
83
75
|
```python
|
|
@@ -282,9 +274,10 @@ finally:
|
|
|
282
274
|
| `name` | `str` | — | **Required** |
|
|
283
275
|
| `shots` | `int` | — | **Required** |
|
|
284
276
|
| `configuration_data` | `Dict[str, Any]` |`None` | |
|
|
285
|
-
| `execution_plan` | `
|
|
286
|
-
| `queue_priority` | `
|
|
277
|
+
| `execution_plan` | `str`, enum, or UUID | `"auto"` | `"auto"`, `"public"`, `"private"`, `ExecutionPlanType`, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
278
|
+
| `queue_priority` | `str`, enum, or UUID | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, `QueuePriorityType`, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
287
279
|
| `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
|
|
280
|
+
| `verbose` | `bool` | `False` | Print submission progress. Same default on every plugin |
|
|
288
281
|
| `job_timeout_seconds` | `int` | `86400` | 1 day |
|
|
289
282
|
| `input_format` | `str` | `"qasm"` | OpenQASM by default; other values only if listed in backend `supported_formats` |
|
|
290
283
|
| `submitted_with` | `str` | `"sdk"` | Client surface for analytics |
|
|
@@ -301,7 +294,10 @@ finally:
|
|
|
301
294
|
| `upload_job_input(file_path=... or file_content=...)` | `str` | Returns upload ID |
|
|
302
295
|
| `prepare_job(JobPreparationCreate)` | `JobPreparationResponse` | Starts preparation |
|
|
303
296
|
| `get_preparation_result(preparation_id)` | `JobPreparationResultResponse` | Includes `.quote` |
|
|
297
|
+
| `wait_for_preparation(preparation_id, timeout=300, interval=2)` | `JobPreparationResultResponse` | Poll until preparation completes |
|
|
298
|
+
| `choose_plan_and_priority(result, execution_plan="auto", queue_priority="auto")` | `PlanSelection` | Friendly names, enums, and UUIDs. `prefer_public=True` ranks Public first when the plan is `"auto"` |
|
|
304
299
|
| `create_job(JobCreate)` | `JobRead` | Submits job |
|
|
300
|
+
| `wait_for_job(job_id, timeout=300, interval=5)` | `JobRead` | Poll until a terminal status. `raise_on_timeout` and `raise_on_failure` default to `True` |
|
|
305
301
|
| `get_job(job_id)` | `JobRead` | Fetch job status |
|
|
306
302
|
| `cancel_job(job_id)` | `None` | Cancel running job |
|
|
307
303
|
| `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
|
|
@@ -353,8 +349,9 @@ from openquantum_sdk.utils import poll_for_status
|
|
|
353
349
|
| ------ | ------ | ------ | ------ |
|
|
354
350
|
| `get_status_fn` | `Callable[[str], Tuple[bool, Any]]` | | Function to be polled |
|
|
355
351
|
| `resource_id` | `str` | | id to be passed to status function: `get_status_fn(resource_id)` |
|
|
356
|
-
| `interval` | `
|
|
357
|
-
| `timeout` | `
|
|
352
|
+
| `interval` | `float` | `5` | Seconds to wait before the next status call |
|
|
353
|
+
| `timeout` | `float` | `300` | Seconds before polling stops |
|
|
354
|
+
| `raise_on_timeout` | `bool` | `True` | Raise `TimeoutError`, or return the last result when `False` |
|
|
358
355
|
|
|
359
356
|
|
|
360
357
|
## Enums
|
|
@@ -455,13 +452,17 @@ for cat in cats:
|
|
|
455
452
|
```
|
|
456
453
|
|
|
457
454
|
|
|
455
|
+
## Live smoke tests
|
|
456
|
+
|
|
457
|
+
`.github/workflows/sdk-smoke.yml` is a manual Actions workflow. It builds the core SDK and the Qiskit, PennyLane, Cirq, and Qrisp plugins from a git ref (empty means `main`), submits a Bell circuit through each one, and waits until the platform job completes. The QPU input defaults to `rigetti:cepheus-1-108q`. Choose the `dev` or `prd` environment when you run it. See [sdk/smoke/README.md](smoke/README.md).
|
|
458
|
+
|
|
458
459
|
## FAQ
|
|
459
460
|
|
|
460
461
|
**Q: How do I get results as JSON?**
|
|
461
462
|
**A:** Use `scheduler.download_job_output(job)` — returns parsed object.
|
|
462
463
|
|
|
463
464
|
**Q: Why does auto mode fail on some QPUs?**
|
|
464
|
-
**A:** `"auto"` selects the cheapest plan and priority (`
|
|
465
|
+
**A:** `"auto"` selects the cheapest plan and priority, which is often Public + Standard. It is not limited to those two. Friendly names (`"public"`, `"private"`, `"standard"`, `"priority"`, `"instant"`), enums, and UUIDs select that option exactly. If you need a specific plan or a faster queue, pass it explicitly. The MCP server passes `prefer_public=True` so Spark credits can apply; `submit_job()` does not.
|
|
465
466
|
|
|
466
467
|
**Q: Can I hide the public plan citation notice?**
|
|
467
468
|
**A:** Setting `OPENQUANTUM_HIDE_CITATION_NOTICE=true` will omit the citation notice from output.
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
from .clients import SchedulerClient, ManagementClient
|
|
2
|
+
from .auth import ClientCredentials, ClientCredentialsAuth
|
|
3
|
+
from .credentials import resolve_env_credentials
|
|
4
|
+
from .session import OpenQuantumClient
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
OpenQuantumError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
CircuitValidationError,
|
|
9
|
+
JobError,
|
|
10
|
+
)
|
|
2
11
|
from .models import (
|
|
3
12
|
APIError,
|
|
4
13
|
ApiErrorType,
|
|
@@ -10,11 +19,19 @@ from .models import (
|
|
|
10
19
|
)
|
|
11
20
|
from .utils import poll_for_status
|
|
12
21
|
|
|
13
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "1.0.0"
|
|
14
23
|
__all__ = [
|
|
15
24
|
"SchedulerClient",
|
|
16
25
|
"ManagementClient",
|
|
26
|
+
"OpenQuantumClient",
|
|
27
|
+
"ClientCredentials",
|
|
28
|
+
"ClientCredentialsAuth",
|
|
29
|
+
"resolve_env_credentials",
|
|
17
30
|
"poll_for_status",
|
|
31
|
+
"OpenQuantumError",
|
|
32
|
+
"AuthenticationError",
|
|
33
|
+
"CircuitValidationError",
|
|
34
|
+
"JobError",
|
|
18
35
|
"APIError",
|
|
19
36
|
"ApiErrorType",
|
|
20
37
|
"TermsOfUseRequiredError",
|
|
@@ -1,48 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
"""CLI entry point for running the full job example."""
|
|
3
3
|
import sys
|
|
4
|
-
import os
|
|
5
4
|
import json
|
|
6
5
|
import argparse
|
|
7
6
|
import requests
|
|
8
7
|
from .auth import ClientCredentials, ClientCredentialsAuth
|
|
9
8
|
from .clients import SchedulerClient, ManagementClient, JobSubmissionConfig
|
|
9
|
+
from .credentials import resolve_env_credentials
|
|
10
10
|
from .models import APIError, TermsOfUseRequiredError, EmailVerificationRequiredError
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def load_creds_from_file(path: str) -> ClientCredentials:
|
|
14
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
15
|
-
data = json.load(f)
|
|
16
|
-
try:
|
|
17
|
-
return ClientCredentials(client_id=data["client_id"], client_secret=data["client_secret"])
|
|
18
|
-
except KeyError as e:
|
|
19
|
-
raise ValueError(f"SDK key file missing field: {e}")
|
|
20
|
-
|
|
21
|
-
|
|
22
13
|
def resolve_credentials(args: argparse.Namespace) -> ClientCredentials:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
14
|
+
"""Resolve client credentials via the shared core resolver.
|
|
15
|
+
|
|
16
|
+
The ``--sdk-key`` flag takes precedence over ``--client-id`` /
|
|
17
|
+
``--client-secret``; both fall back to the environment.
|
|
18
|
+
"""
|
|
19
|
+
# Honor the --sdk-key flag ahead of direct client id/secret args.
|
|
20
|
+
client_id = None if args.sdk_key else args.client_id
|
|
21
|
+
client_secret = None if args.sdk_key else args.client_secret
|
|
22
|
+
try:
|
|
23
|
+
_token, creds = resolve_env_credentials(
|
|
24
|
+
client_id=client_id,
|
|
25
|
+
client_secret=client_secret,
|
|
26
|
+
sdk_key_path=args.sdk_key,
|
|
27
|
+
require=True,
|
|
28
|
+
)
|
|
29
|
+
except ValueError as e:
|
|
30
|
+
raise SystemExit(str(e))
|
|
31
|
+
if creds is None:
|
|
32
|
+
raise SystemExit(
|
|
33
|
+
"The SDK CLI requires client credentials (SDK key), not a token."
|
|
34
|
+
)
|
|
35
|
+
return creds
|
|
46
36
|
|
|
47
37
|
|
|
48
38
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Account persistence for the Open Quantum SDK.
|
|
2
|
+
|
|
3
|
+
Stores named authentication accounts (bearer tokens or OAuth2 client
|
|
4
|
+
credentials) in a JSON file at a platform-appropriate location, with secrets
|
|
5
|
+
kept in the system keyring when one is available. This logic is
|
|
6
|
+
framework-agnostic and is shared by every SDK surface (core client, Qiskit,
|
|
7
|
+
PennyLane, Qrisp, CLI).
|
|
8
|
+
|
|
9
|
+
The on-disk format and keyring key scheme are intentionally stable: a file
|
|
10
|
+
written by an older release remains readable here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from tempfile import NamedTemporaryFile
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
from openquantum_sdk.auth import ClientCredentials
|
|
22
|
+
|
|
23
|
+
DEFAULT_KEYCLOAK_BASE = "https://id.openquantum.com"
|
|
24
|
+
DEFAULT_REALM = "platform"
|
|
25
|
+
DEFAULT_SCHEDULER_URL = "https://scheduler.openquantum.com"
|
|
26
|
+
DEFAULT_MANAGEMENT_URL = "https://management.openquantum.com"
|
|
27
|
+
|
|
28
|
+
_KEYRING_SERVICE = "openquantum"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def accounts_path(filename: Optional[str] = None) -> Path:
|
|
32
|
+
"""Resolve the accounts file path.
|
|
33
|
+
|
|
34
|
+
Order of precedence: explicit ``filename`` → ``OPENQUANTUM_CONFIG_DIR`` →
|
|
35
|
+
the platform user config dir → ``~/.openquantum/accounts.json``.
|
|
36
|
+
"""
|
|
37
|
+
if filename:
|
|
38
|
+
return Path(filename).expanduser()
|
|
39
|
+
cfg_dir = os.getenv("OPENQUANTUM_CONFIG_DIR")
|
|
40
|
+
if cfg_dir:
|
|
41
|
+
return Path(cfg_dir).expanduser() / "accounts.json"
|
|
42
|
+
try:
|
|
43
|
+
from platformdirs import PlatformDirs
|
|
44
|
+
|
|
45
|
+
d = PlatformDirs(appname="OpenQuantum", appauthor=False)
|
|
46
|
+
return Path(d.user_config_dir) / "accounts.json"
|
|
47
|
+
except Exception:
|
|
48
|
+
return Path(os.path.expanduser("~/.openquantum/accounts.json"))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ensure_cfg_dir(path: Path) -> None:
|
|
52
|
+
try:
|
|
53
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def save_account(
|
|
59
|
+
*,
|
|
60
|
+
name: str = "default",
|
|
61
|
+
token: Optional[str] = None,
|
|
62
|
+
creds: Optional[ClientCredentials] = None,
|
|
63
|
+
keycloak_base: str = DEFAULT_KEYCLOAK_BASE,
|
|
64
|
+
realm: str = DEFAULT_REALM,
|
|
65
|
+
scheduler_url: str = DEFAULT_SCHEDULER_URL,
|
|
66
|
+
management_url: str = DEFAULT_MANAGEMENT_URL,
|
|
67
|
+
filename: Optional[str] = None,
|
|
68
|
+
overwrite: bool = True,
|
|
69
|
+
use_keyring: bool = True,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Persist account credentials to disk for later reuse.
|
|
72
|
+
|
|
73
|
+
Secrets (tokens, client secrets) are stored in the system keyring when
|
|
74
|
+
available; otherwise they are written to the JSON file directly.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If both ``token`` and ``creds`` are provided, or if
|
|
78
|
+
``overwrite`` is ``False`` and the account already exists.
|
|
79
|
+
"""
|
|
80
|
+
if token and creds:
|
|
81
|
+
raise ValueError("Provide either 'token' or 'creds', not both.")
|
|
82
|
+
path = accounts_path(filename)
|
|
83
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
data: Dict[str, Any] = {}
|
|
85
|
+
if path.exists():
|
|
86
|
+
try:
|
|
87
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
88
|
+
except Exception:
|
|
89
|
+
if not overwrite:
|
|
90
|
+
raise
|
|
91
|
+
data = {}
|
|
92
|
+
if not overwrite and name in data:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Account '{name}' already exists. Set overwrite=True to replace."
|
|
95
|
+
)
|
|
96
|
+
entry: Dict[str, Any] = {
|
|
97
|
+
"auth_mode": "token" if token else "client_credentials",
|
|
98
|
+
"token": token,
|
|
99
|
+
"creds": (
|
|
100
|
+
{"client_id": creds.client_id, "client_secret": creds.client_secret}
|
|
101
|
+
if creds
|
|
102
|
+
else None
|
|
103
|
+
),
|
|
104
|
+
"keycloak_base": keycloak_base,
|
|
105
|
+
"realm": realm,
|
|
106
|
+
"scheduler_url": scheduler_url,
|
|
107
|
+
"management_url": management_url,
|
|
108
|
+
}
|
|
109
|
+
if use_keyring:
|
|
110
|
+
try:
|
|
111
|
+
import keyring
|
|
112
|
+
|
|
113
|
+
if token:
|
|
114
|
+
keyring.set_password(_KEYRING_SERVICE, f"{name}:token", token)
|
|
115
|
+
entry["token"] = None
|
|
116
|
+
if creds and creds.client_secret:
|
|
117
|
+
keyring.set_password(
|
|
118
|
+
_KEYRING_SERVICE, f"{name}:client_secret", creds.client_secret
|
|
119
|
+
)
|
|
120
|
+
entry["creds"] = {
|
|
121
|
+
"client_id": creds.client_id,
|
|
122
|
+
"client_secret": None,
|
|
123
|
+
}
|
|
124
|
+
entry["keyring"] = True
|
|
125
|
+
except Exception:
|
|
126
|
+
entry.setdefault("keyring", False)
|
|
127
|
+
data[name] = entry
|
|
128
|
+
|
|
129
|
+
tmp_txt = json.dumps(data, indent=2)
|
|
130
|
+
_ensure_cfg_dir(path)
|
|
131
|
+
with NamedTemporaryFile(
|
|
132
|
+
"w", delete=False, encoding="utf-8", dir=str(path.parent)
|
|
133
|
+
) as tmp:
|
|
134
|
+
tmp.write(tmp_txt)
|
|
135
|
+
tmp.flush()
|
|
136
|
+
os.fsync(tmp.fileno())
|
|
137
|
+
tmp_path = Path(tmp.name)
|
|
138
|
+
tmp_path.replace(path)
|
|
139
|
+
try:
|
|
140
|
+
os.chmod(path, 0o600)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def saved_accounts(*, filename: Optional[str] = None) -> Dict[str, Any]:
|
|
146
|
+
"""Return all saved accounts with secrets masked.
|
|
147
|
+
|
|
148
|
+
Tokens and client secrets are truncated so that only the first and last
|
|
149
|
+
few characters remain visible. Returns an empty dict if no accounts file
|
|
150
|
+
exists or it cannot be parsed.
|
|
151
|
+
"""
|
|
152
|
+
path = accounts_path(filename)
|
|
153
|
+
if not path.exists():
|
|
154
|
+
return {}
|
|
155
|
+
try:
|
|
156
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
157
|
+
masked: Dict[str, Any] = {}
|
|
158
|
+
for k, v in raw.items():
|
|
159
|
+
v = dict(v)
|
|
160
|
+
tok = v.get("token")
|
|
161
|
+
if tok:
|
|
162
|
+
v["token"] = tok[:6] + "…" + tok[-4:]
|
|
163
|
+
creds = v.get("creds") or {}
|
|
164
|
+
if creds.get("client_secret"):
|
|
165
|
+
cs = creds["client_secret"]
|
|
166
|
+
creds["client_secret"] = cs[:4] + "…" + cs[-4:]
|
|
167
|
+
v["creds"] = creds
|
|
168
|
+
masked[k] = v
|
|
169
|
+
return masked
|
|
170
|
+
except Exception:
|
|
171
|
+
return {}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def delete_account(*, name: str = "default", filename: Optional[str] = None) -> bool:
|
|
175
|
+
"""Delete a saved account by name.
|
|
176
|
+
|
|
177
|
+
Returns ``True`` if the account was found and deleted, ``False`` otherwise.
|
|
178
|
+
"""
|
|
179
|
+
path = accounts_path(filename)
|
|
180
|
+
if not path.exists():
|
|
181
|
+
return False
|
|
182
|
+
try:
|
|
183
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
184
|
+
except Exception:
|
|
185
|
+
return False
|
|
186
|
+
if name not in data:
|
|
187
|
+
return False
|
|
188
|
+
try:
|
|
189
|
+
import keyring
|
|
190
|
+
|
|
191
|
+
keyring.delete_password(_KEYRING_SERVICE, f"{name}:token")
|
|
192
|
+
keyring.delete_password(_KEYRING_SERVICE, f"{name}:client_secret")
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
del data[name]
|
|
196
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def load_account(
|
|
201
|
+
name: str = "default", *, filename: Optional[str] = None
|
|
202
|
+
) -> Optional[Dict[str, Any]]:
|
|
203
|
+
"""Load a saved account and resolve its secrets from the keyring.
|
|
204
|
+
|
|
205
|
+
Returns a dict with keys ``auth_mode``, ``token`` (or ``None``), ``creds``
|
|
206
|
+
(a :class:`ClientCredentials` or ``None``), ``keycloak_base``, ``realm``,
|
|
207
|
+
``scheduler_url``, and ``management_url``. Returns ``None`` if the file or
|
|
208
|
+
the named account does not exist.
|
|
209
|
+
"""
|
|
210
|
+
path = accounts_path(filename)
|
|
211
|
+
if not path.exists():
|
|
212
|
+
return None
|
|
213
|
+
try:
|
|
214
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
if name not in data:
|
|
218
|
+
return None
|
|
219
|
+
entry = data[name]
|
|
220
|
+
kr = bool(entry.get("keyring", False))
|
|
221
|
+
|
|
222
|
+
token: Optional[str] = None
|
|
223
|
+
creds: Optional[ClientCredentials] = None
|
|
224
|
+
if entry.get("auth_mode") == "token":
|
|
225
|
+
token = entry.get("token")
|
|
226
|
+
if (not token) and kr:
|
|
227
|
+
try:
|
|
228
|
+
import keyring
|
|
229
|
+
|
|
230
|
+
token = keyring.get_password(_KEYRING_SERVICE, f"{name}:token")
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
else:
|
|
234
|
+
c = entry.get("creds") or {}
|
|
235
|
+
if c.get("client_id"):
|
|
236
|
+
secret = c.get("client_secret") or ""
|
|
237
|
+
if (not secret) and kr:
|
|
238
|
+
try:
|
|
239
|
+
import keyring
|
|
240
|
+
|
|
241
|
+
maybe = keyring.get_password(
|
|
242
|
+
_KEYRING_SERVICE, f"{name}:client_secret"
|
|
243
|
+
)
|
|
244
|
+
secret = maybe or secret
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
creds = ClientCredentials(client_id=c.get("client_id"), client_secret=secret)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"auth_mode": entry.get("auth_mode"),
|
|
251
|
+
"token": token,
|
|
252
|
+
"creds": creds,
|
|
253
|
+
"keycloak_base": entry.get("keycloak_base", DEFAULT_KEYCLOAK_BASE),
|
|
254
|
+
"realm": entry.get("realm", DEFAULT_REALM),
|
|
255
|
+
"scheduler_url": entry.get("scheduler_url", DEFAULT_SCHEDULER_URL),
|
|
256
|
+
"management_url": entry.get("management_url", DEFAULT_MANAGEMENT_URL),
|
|
257
|
+
}
|