openquantum-sdk 0.3.4__tar.gz → 0.4.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.
Files changed (33) hide show
  1. {openquantum_sdk-0.3.4/openquantum_sdk.egg-info → openquantum_sdk-0.4.0}/PKG-INFO +28 -13
  2. openquantum_sdk-0.3.4/PKG-INFO → openquantum_sdk-0.4.0/README.md +23 -23
  3. openquantum_sdk-0.4.0/openquantum_sdk/__init__.py +21 -0
  4. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk/__main__.py +20 -3
  5. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk/auth.py +58 -5
  6. openquantum_sdk-0.4.0/openquantum_sdk/backends.py +706 -0
  7. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk/clients.py +379 -29
  8. openquantum_sdk-0.4.0/openquantum_sdk/enums.py +40 -0
  9. openquantum_sdk-0.4.0/openquantum_sdk/models.py +560 -0
  10. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk/qiskit/__init__.py +3 -7
  11. openquantum_sdk-0.4.0/openquantum_sdk/utils.py +71 -0
  12. openquantum_sdk-0.3.4/README.md → openquantum_sdk-0.4.0/openquantum_sdk.egg-info/PKG-INFO +38 -12
  13. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/SOURCES.txt +3 -1
  14. openquantum_sdk-0.4.0/openquantum_sdk.egg-info/requires.txt +7 -0
  15. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/pyproject.toml +5 -1
  16. openquantum_sdk-0.4.0/tests/test_backends.py +257 -0
  17. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/tests/test_clients.py +105 -2
  18. openquantum_sdk-0.4.0/tests/test_clients_plan_priority.py +162 -0
  19. openquantum_sdk-0.3.4/openquantum_sdk/__init__.py +0 -5
  20. openquantum_sdk-0.3.4/openquantum_sdk/enums.py +0 -16
  21. openquantum_sdk-0.3.4/openquantum_sdk/models.py +0 -230
  22. openquantum_sdk-0.3.4/openquantum_sdk/utils.py +0 -24
  23. openquantum_sdk-0.3.4/openquantum_sdk.egg-info/requires.txt +0 -1
  24. openquantum_sdk-0.3.4/tests/conftest.py +0 -40
  25. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/MANIFEST.in +0 -0
  26. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/dependency_links.txt +0 -0
  27. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/entry_points.txt +0 -0
  28. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/top_level.txt +0 -0
  29. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/requirements-dev.txt +0 -0
  30. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/requirements.txt +0 -0
  31. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/setup.cfg +0 -0
  32. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/tests/__init__.py +0 -0
  33. {openquantum_sdk-0.3.4 → openquantum_sdk-0.4.0}/tests/test_qiskit_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openquantum-sdk
3
- Version: 0.3.4
3
+ Version: 0.4.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
@@ -8,6 +8,10 @@ Classifier: Operating System :: OS Independent
8
8
  Requires-Python: >=3.8
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: requests>=2.25.0
11
+ Provides-Extra: qiskit
12
+ Requires-Dist: openquantum-sdk-qiskit>=0.3.0; extra == "qiskit"
13
+ Provides-Extra: pennylane
14
+ Requires-Dist: openquantum-sdk-pennylane>=0.2.0; extra == "pennylane"
11
15
 
12
16
  # Open Quantum Python SDK
13
17
 
@@ -17,9 +21,11 @@ A lightweight, modern Python SDK for the **Open Quantum Platform**.
17
21
 
18
22
  * **Management API** — list organizations, backend classes, and providers
19
23
  * **Scheduler API** — job categories, upload, prepare, submit, poll, and **one-call `submit_job()`**
24
+ * **Input formats** — OpenQASM (`qasm`) by default; other formats via `input_format` when the backend supports them
20
25
  * **Smart defaults** — auto-select execution plan & queue priority from quote
21
26
  * **Thread-safe auth** — Keycloak client-credentials with auto-refresh
22
27
  * **Automatic output download** — `download_job_output()` returns parsed JSON
28
+ * **Calibration download** — `download_job_calibration()` when the job has `calibration_data_url`
23
29
 
24
30
  ## Installation
25
31
 
@@ -35,7 +41,7 @@ Python 3.9+ required.
35
41
 
36
42
  ### Option 1: SDK Key File (Recommended)
37
43
 
38
- Download from [Open Quantum SDK Keys](https://openquantum.com/keys):
44
+ Download from [Open Quantum SDK Keys](https://www.openquantum.com/keys):
39
45
 
40
46
  ```json
41
47
  {
@@ -100,7 +106,7 @@ try:
100
106
  # 3. Define config
101
107
  config = JobSubmissionConfig(
102
108
  organization_id=org_id,
103
- backend_class_id="rigetti:ankaa-3",
109
+ backend_class_id="iqm:emerald",
104
110
  name="Bell State SDK Quickstart",
105
111
  job_subcategory_id="fin:port", # Finance Portfolio Optimization
106
112
  shots=100,
@@ -126,6 +132,11 @@ try:
126
132
  else:
127
133
  print("No output available.")
128
134
 
135
+ if job.calibration_data_url:
136
+ cal_json = scheduler.download_job_calibration(job)
137
+ print("\n--- CALIBRATION (JSON) ---")
138
+ print(json.dumps(cal_json, indent=2))
139
+
129
140
  except Exception as e:
130
141
  print(f"\n--- JOB FAILED ---")
131
142
  print(f"Error: {e}")
@@ -162,7 +173,7 @@ try:
162
173
  print(f" - {bc.name} [{bc.type}] — {bc.short_code or bc.id}")
163
174
 
164
175
  # 2. Choose IDs
165
- MY_BACKEND = "rigetti:ankaa-3"
176
+ MY_BACKEND = "iqm:emerald"
166
177
  MY_SUBCATEGORY = "fin:port"
167
178
 
168
179
  # 3. Upload
@@ -269,6 +280,8 @@ finally:
269
280
  | `queue_priority` | `QueuePriorityType \| "auto"` | `"auto"` | |
270
281
  | `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
271
282
  | `job_timeout_seconds` | `int` | `86400` | 1 day |
283
+ | `input_format` | `str` | `"qasm"` | OpenQASM by default; other values only if listed in backend `supported_formats` |
284
+ | `submitted_with` | `str` | `"sdk"` | Client surface for analytics |
272
285
 
273
286
  ## API Reference
274
287
 
@@ -287,6 +300,7 @@ finally:
287
300
  | `cancel_job(job_id)` | `None` | Cancel running job |
288
301
  | `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
289
302
  | **`download_job_output(job)`** | `Any` | **Downloads + parses JSON output** |
303
+ | **`download_job_calibration(job)`** | `Any` | **Downloads + parses calibration JSON** (when tagged) |
290
304
  | `submit_job(config, file_path=... or file_content=...)` | `JobRead` | **One-call submission + polling** |
291
305
 
292
306
  ### `ManagementClient`
@@ -304,15 +318,15 @@ finally:
304
318
  | Model | Key Fields |
305
319
  |-------|------------|
306
320
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
307
- | `ErrorResponse` | `status_code`, `message: List[str]`, `error_code` |
321
+ | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
308
322
  | `JobCategoryRead` | `id`, `name`, `short_code` |
309
323
  | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
310
- | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `credits_used`, `submitted_at` |
311
- | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data` |
324
+ | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
325
+ | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
312
326
  | `JobPreparationResponse` | `id` |
313
327
  | `JobPreparationResultResponse` | `organization_id`, `backend_class_id`, `status`, `name`, `input_data_url`, `job_category_id`, `job_subcategory_id`, `shots`, `quote: List[QuotePlan]`, `message`, `configuration_data` |
314
328
  | `JobPreparationUploadResponse` | `id`, `url` |
315
- | `JobRead` | `id`, `status`, `input_data_url`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, `message`, `output_data_url`, `transaction_id`, `submitted_at` |
329
+ | `JobRead` | `id`, `status`, `input_data_url`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, `message`, `output_data_url`, `calibration_data_url`, `transaction_id`, `submitted_at` |
316
330
  | `OrganizationRead` | `id`, `name` |
317
331
  | `PaginatedBackendClasses` | `backend_classes: List[BackendClassRead]`, `pagination: PaginationInfo` |
318
332
  | `PaginatedJobCategories` | `categories: List[JobCategoryRead]`, `pagination: PaginationInfo` |
@@ -353,7 +367,7 @@ from openquantum_sdk.enums import ExecutionPlanType, QueuePriorityType
353
367
  ```bash
354
368
  python -m openquantum_sdk --sdk-key ./key.json \
355
369
  --input bell.qasm \
356
- --backend "ionq:aria-1" \
370
+ --backend "iqm:emerald" \
357
371
  --name "CLI Circuit" \
358
372
  --subcategory "mathematics:linear-systems" \
359
373
  --shots 100 \
@@ -367,11 +381,12 @@ Automatically **downloads and pretty-prints JSON output**.
367
381
  ### Backends
368
382
  | Provider | Backend | Short Code |
369
383
  | ------ | ------ | ------ |
370
- | IonQ | Aria-1 | `ionq:aria-1` |
384
+ | AQT | Ibex-Q1 | `aqt:ibex-q1` |
385
+ | IonQ | Forte Enterprise | `ionq:forte-ent` |
371
386
  | IonQ | Forte-1 | `ionq:forte-1` |
372
387
  | IQM | Emerald | `iqm:emerald` |
373
388
  | IQM | Garnet | `iqm:garnet` |
374
- | Rigetti | Ankaa-3 | `rigetti:ankaa-3` |
389
+ | Rigetti | Cepheus-1-108Q | `rigetti:cepheus-108q`|
375
390
 
376
391
  *Note: This list may be modified as backends are added or removed from the platform. For the most up-to-date list, run the following code:*
377
392
 
@@ -439,8 +454,8 @@ for cat in cats:
439
454
  **Q: How do I get results as JSON?**
440
455
  **A:** Use `scheduler.download_job_output(job)` — returns parsed object.
441
456
 
442
- **Q: Why does auto mode fail on some QPUs?**
443
- **A:** `auto` only allows `PUBLIC` + `STANDARD`. Use explicit enums for private/instant access.
457
+ **Q: Why does auto mode fail on some QPUs?**
458
+ **A:** `"auto"` selects the cheapest plan and priority (`PUBLIC` + `STANDARD`). If a backend only offers private execution, pass `execution_plan="private"` explicitly. Similarly, use `queue_priority="priority"` or `"instant"` for higher-priority queue access.
444
459
 
445
460
  **Q: Can I hide the public plan citation notice?**
446
461
  **A:** Setting `OPENQUANTUM_HIDE_CITATION_NOTICE=true` will omit the citation notice from output.
@@ -1,14 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: openquantum-sdk
3
- Version: 0.3.4
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
-
12
1
  # Open Quantum Python SDK
13
2
 
14
3
  A lightweight, modern Python SDK for the **Open Quantum Platform**.
@@ -17,9 +6,11 @@ A lightweight, modern Python SDK for the **Open Quantum Platform**.
17
6
 
18
7
  * **Management API** — list organizations, backend classes, and providers
19
8
  * **Scheduler API** — job categories, upload, prepare, submit, poll, and **one-call `submit_job()`**
9
+ * **Input formats** — OpenQASM (`qasm`) by default; other formats via `input_format` when the backend supports them
20
10
  * **Smart defaults** — auto-select execution plan & queue priority from quote
21
11
  * **Thread-safe auth** — Keycloak client-credentials with auto-refresh
22
12
  * **Automatic output download** — `download_job_output()` returns parsed JSON
13
+ * **Calibration download** — `download_job_calibration()` when the job has `calibration_data_url`
23
14
 
24
15
  ## Installation
25
16
 
@@ -35,7 +26,7 @@ Python 3.9+ required.
35
26
 
36
27
  ### Option 1: SDK Key File (Recommended)
37
28
 
38
- Download from [Open Quantum SDK Keys](https://openquantum.com/keys):
29
+ Download from [Open Quantum SDK Keys](https://www.openquantum.com/keys):
39
30
 
40
31
  ```json
41
32
  {
@@ -100,7 +91,7 @@ try:
100
91
  # 3. Define config
101
92
  config = JobSubmissionConfig(
102
93
  organization_id=org_id,
103
- backend_class_id="rigetti:ankaa-3",
94
+ backend_class_id="iqm:emerald",
104
95
  name="Bell State SDK Quickstart",
105
96
  job_subcategory_id="fin:port", # Finance Portfolio Optimization
106
97
  shots=100,
@@ -126,6 +117,11 @@ try:
126
117
  else:
127
118
  print("No output available.")
128
119
 
120
+ if job.calibration_data_url:
121
+ cal_json = scheduler.download_job_calibration(job)
122
+ print("\n--- CALIBRATION (JSON) ---")
123
+ print(json.dumps(cal_json, indent=2))
124
+
129
125
  except Exception as e:
130
126
  print(f"\n--- JOB FAILED ---")
131
127
  print(f"Error: {e}")
@@ -162,7 +158,7 @@ try:
162
158
  print(f" - {bc.name} [{bc.type}] — {bc.short_code or bc.id}")
163
159
 
164
160
  # 2. Choose IDs
165
- MY_BACKEND = "rigetti:ankaa-3"
161
+ MY_BACKEND = "iqm:emerald"
166
162
  MY_SUBCATEGORY = "fin:port"
167
163
 
168
164
  # 3. Upload
@@ -269,6 +265,8 @@ finally:
269
265
  | `queue_priority` | `QueuePriorityType \| "auto"` | `"auto"` | |
270
266
  | `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
271
267
  | `job_timeout_seconds` | `int` | `86400` | 1 day |
268
+ | `input_format` | `str` | `"qasm"` | OpenQASM by default; other values only if listed in backend `supported_formats` |
269
+ | `submitted_with` | `str` | `"sdk"` | Client surface for analytics |
272
270
 
273
271
  ## API Reference
274
272
 
@@ -287,6 +285,7 @@ finally:
287
285
  | `cancel_job(job_id)` | `None` | Cancel running job |
288
286
  | `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
289
287
  | **`download_job_output(job)`** | `Any` | **Downloads + parses JSON output** |
288
+ | **`download_job_calibration(job)`** | `Any` | **Downloads + parses calibration JSON** (when tagged) |
290
289
  | `submit_job(config, file_path=... or file_content=...)` | `JobRead` | **One-call submission + polling** |
291
290
 
292
291
  ### `ManagementClient`
@@ -304,15 +303,15 @@ finally:
304
303
  | Model | Key Fields |
305
304
  |-------|------------|
306
305
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
307
- | `ErrorResponse` | `status_code`, `message: List[str]`, `error_code` |
306
+ | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
308
307
  | `JobCategoryRead` | `id`, `name`, `short_code` |
309
308
  | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
310
- | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `credits_used`, `submitted_at` |
311
- | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data` |
309
+ | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
310
+ | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
312
311
  | `JobPreparationResponse` | `id` |
313
312
  | `JobPreparationResultResponse` | `organization_id`, `backend_class_id`, `status`, `name`, `input_data_url`, `job_category_id`, `job_subcategory_id`, `shots`, `quote: List[QuotePlan]`, `message`, `configuration_data` |
314
313
  | `JobPreparationUploadResponse` | `id`, `url` |
315
- | `JobRead` | `id`, `status`, `input_data_url`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, `message`, `output_data_url`, `transaction_id`, `submitted_at` |
314
+ | `JobRead` | `id`, `status`, `input_data_url`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, `message`, `output_data_url`, `calibration_data_url`, `transaction_id`, `submitted_at` |
316
315
  | `OrganizationRead` | `id`, `name` |
317
316
  | `PaginatedBackendClasses` | `backend_classes: List[BackendClassRead]`, `pagination: PaginationInfo` |
318
317
  | `PaginatedJobCategories` | `categories: List[JobCategoryRead]`, `pagination: PaginationInfo` |
@@ -353,7 +352,7 @@ from openquantum_sdk.enums import ExecutionPlanType, QueuePriorityType
353
352
  ```bash
354
353
  python -m openquantum_sdk --sdk-key ./key.json \
355
354
  --input bell.qasm \
356
- --backend "ionq:aria-1" \
355
+ --backend "iqm:emerald" \
357
356
  --name "CLI Circuit" \
358
357
  --subcategory "mathematics:linear-systems" \
359
358
  --shots 100 \
@@ -367,11 +366,12 @@ Automatically **downloads and pretty-prints JSON output**.
367
366
  ### Backends
368
367
  | Provider | Backend | Short Code |
369
368
  | ------ | ------ | ------ |
370
- | IonQ | Aria-1 | `ionq:aria-1` |
369
+ | AQT | Ibex-Q1 | `aqt:ibex-q1` |
370
+ | IonQ | Forte Enterprise | `ionq:forte-ent` |
371
371
  | IonQ | Forte-1 | `ionq:forte-1` |
372
372
  | IQM | Emerald | `iqm:emerald` |
373
373
  | IQM | Garnet | `iqm:garnet` |
374
- | Rigetti | Ankaa-3 | `rigetti:ankaa-3` |
374
+ | Rigetti | Cepheus-1-108Q | `rigetti:cepheus-108q`|
375
375
 
376
376
  *Note: This list may be modified as backends are added or removed from the platform. For the most up-to-date list, run the following code:*
377
377
 
@@ -439,8 +439,8 @@ for cat in cats:
439
439
  **Q: How do I get results as JSON?**
440
440
  **A:** Use `scheduler.download_job_output(job)` — returns parsed object.
441
441
 
442
- **Q: Why does auto mode fail on some QPUs?**
443
- **A:** `auto` only allows `PUBLIC` + `STANDARD`. Use explicit enums for private/instant access.
442
+ **Q: Why does auto mode fail on some QPUs?**
443
+ **A:** `"auto"` selects the cheapest plan and priority (`PUBLIC` + `STANDARD`). If a backend only offers private execution, pass `execution_plan="private"` explicitly. Similarly, use `queue_priority="priority"` or `"instant"` for higher-priority queue access.
444
444
 
445
445
  **Q: Can I hide the public plan citation notice?**
446
446
  **A:** Setting `OPENQUANTUM_HIDE_CITATION_NOTICE=true` will omit the citation notice from output.
@@ -0,0 +1,21 @@
1
+ from .clients import SchedulerClient, ManagementClient
2
+ from .models import (
3
+ APIError,
4
+ ApiErrorType,
5
+ TermsOfUseRequiredError,
6
+ TERMS_OF_USE_REQUIRED_MESSAGE,
7
+ is_not_found_error,
8
+ )
9
+ from .utils import poll_for_status
10
+
11
+ __version__ = "0.4.0"
12
+ __all__ = [
13
+ "SchedulerClient",
14
+ "ManagementClient",
15
+ "poll_for_status",
16
+ "APIError",
17
+ "ApiErrorType",
18
+ "TermsOfUseRequiredError",
19
+ "TERMS_OF_USE_REQUIRED_MESSAGE",
20
+ "is_not_found_error",
21
+ ]
@@ -6,7 +6,8 @@ import json
6
6
  import argparse
7
7
  import requests
8
8
  from .auth import ClientCredentials, ClientCredentialsAuth
9
- from .clients import APIError, SchedulerClient, ManagementClient, JobSubmissionConfig
9
+ from .clients import SchedulerClient, ManagementClient, JobSubmissionConfig
10
+ from .models import APIError, TermsOfUseRequiredError
10
11
 
11
12
 
12
13
  def load_creds_from_file(path: str) -> ClientCredentials:
@@ -121,8 +122,10 @@ def main():
121
122
  creds=creds
122
123
  )
123
124
 
124
- scheduler = SchedulerClient(base_url=args.scheduler_base, auth=auth)
125
125
  management = ManagementClient(base_url=args.management_base, auth=auth)
126
+ scheduler = SchedulerClient(
127
+ base_url=args.scheduler_base, auth=auth, management_client=management
128
+ )
126
129
 
127
130
  try:
128
131
  print("--- Open Quantum Job Submission CLI ---")
@@ -161,12 +164,13 @@ def main():
161
164
  print(f"Final Status: {job.status}")
162
165
  print(f"Submitted At: {job.submitted_at}")
163
166
  print(f"Output URL: {job.output_data_url}")
167
+ print(f"Calibration URL: {job.calibration_data_url}")
164
168
  print("---------------------")
165
169
 
166
170
  if job.output_data_url:
167
171
  print("\nFetching job output...")
168
172
  try:
169
- output_json = scheduler.download_job_output(job.id)
173
+ output_json = scheduler.download_job_output(job)
170
174
  print("\n--- JOB OUTPUT (JSON) ---")
171
175
  print(json.dumps(output_json, indent=2))
172
176
  print("-------------------------")
@@ -175,6 +179,19 @@ def main():
175
179
  else:
176
180
  print("\nNo output_data_url available (job may have failed or produced no output).")
177
181
 
182
+ if job.calibration_data_url:
183
+ print("\nFetching calibration data...")
184
+ try:
185
+ cal_json = scheduler.download_job_calibration(job)
186
+ print("\n--- CALIBRATION (JSON) ---")
187
+ print(json.dumps(cal_json, indent=2))
188
+ print("--------------------------")
189
+ except Exception as e:
190
+ print(f"Failed to download/parse calibration: {e}", file=sys.stderr)
191
+
192
+ except TermsOfUseRequiredError as e:
193
+ print(str(e), file=sys.stderr)
194
+ sys.exit(1)
178
195
  except requests.exceptions.HTTPError as e:
179
196
  print(f"API error: {e.response.status_code} - {e.response.text}", file=sys.stderr)
180
197
  sys.exit(1)
@@ -9,17 +9,28 @@ from urllib.parse import urljoin
9
9
 
10
10
  @dataclass
11
11
  class ClientCredentials:
12
+ """SDK key credentials for machine-to-machine authentication.
13
+
14
+ Attributes:
15
+ client_id: The OAuth2 client ID (SDK key ID).
16
+ client_secret: The OAuth2 client secret (SDK key secret).
17
+ """
18
+
12
19
  client_id: str
13
20
  client_secret: str
14
21
 
15
22
 
16
23
  class ClientCredentialsAuth:
24
+ """Keycloak client-credentials OAuth2 helper with thread-safe token caching.
25
+
26
+ Manages the full token lifecycle: initial fetch, automatic refresh when
27
+ the token is within ``leeway_seconds`` of expiry, and forced refresh on
28
+ 401 retries. All token operations are thread-safe.
29
+
30
+ The token endpoint is constructed as:
31
+ ``{keycloak_base}/realms/{realm}/protocol/openid-connect/token``
17
32
  """
18
- Keycloak client-credentials OAuth2 helper with local, thread-safe token cache.
19
- - Fetches token from: {keycloak_base}/realms/{realm}/protocol/openid-connect/token
20
- - Auto refreshes when within leeway of expiry
21
- - Retries once on 401
22
- """
33
+
23
34
  def __init__(
24
35
  self,
25
36
  creds: ClientCredentials,
@@ -29,6 +40,19 @@ class ClientCredentialsAuth:
29
40
  leeway_seconds: int = 30,
30
41
  session: Optional[requests.Session] = None,
31
42
  ):
43
+ """Initialize the auth helper.
44
+
45
+ Args:
46
+ creds: Client credentials containing the client ID and secret.
47
+ keycloak_base: Base URL of the Keycloak server.
48
+ Defaults to ``"https://id.openquantum.com"``.
49
+ realm: Keycloak realm name. Defaults to ``"platform"``.
50
+ scope: Optional OAuth2 scope to request.
51
+ leeway_seconds: Seconds before token expiry to trigger a
52
+ proactive refresh. Defaults to 30.
53
+ session: Optional ``requests.Session`` to use for token requests.
54
+ A new session is created if not provided.
55
+ """
32
56
  self.keycloak_base = keycloak_base.rstrip("/")
33
57
  self.realm = realm
34
58
  self.creds = creds
@@ -41,6 +65,7 @@ class ClientCredentialsAuth:
41
65
 
42
66
  @property
43
67
  def token_endpoint(self) -> str:
68
+ """The full Keycloak token endpoint URL for this realm."""
44
69
  return urljoin(
45
70
  self.keycloak_base + "/",
46
71
  f"realms/{self.realm}/protocol/openid-connect/token",
@@ -72,6 +97,22 @@ class ClientCredentialsAuth:
72
97
  self._expires_at = time.time() + int(expires_in)
73
98
 
74
99
  def get_access_token(self, force: bool = False) -> str:
100
+ """Return a valid access token, refreshing if needed.
101
+
102
+ Uses double-checked locking to ensure only one thread fetches a
103
+ new token at a time.
104
+
105
+ Args:
106
+ force: If ``True``, forces a token refresh regardless of
107
+ expiry status. Defaults to ``False``.
108
+
109
+ Returns:
110
+ A valid OAuth2 access token string.
111
+
112
+ Raises:
113
+ RuntimeError: If the Keycloak response is missing an access token.
114
+ requests.HTTPError: If the token request fails.
115
+ """
75
116
  if force or self._needs_refresh():
76
117
  with self._lock:
77
118
  if force or self._needs_refresh():
@@ -79,6 +120,18 @@ class ClientCredentialsAuth:
79
120
  return self._access_token # type: ignore[str-bytes-safe]
80
121
 
81
122
  def apply_auth_header(self, headers: Dict[str, str]) -> Dict[str, str]:
123
+ """Add a Bearer authorization header using the current access token.
124
+
125
+ Does not mutate the input dict; returns a new dict with the
126
+ ``Authorization`` header added.
127
+
128
+ Args:
129
+ headers: Existing headers to augment.
130
+
131
+ Returns:
132
+ A new headers dict containing all entries from ``headers`` plus
133
+ the ``Authorization: Bearer <token>`` header.
134
+ """
82
135
  token = self.get_access_token()
83
136
  # don't mutate caller's header dict
84
137
  return {**headers, "Authorization": f"Bearer {token}"}