openquantum-sdk 0.3.7__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 (29) hide show
  1. {openquantum_sdk-0.3.7/openquantum_sdk.egg-info → openquantum_sdk-0.4.0}/PKG-INFO +18 -8
  2. openquantum_sdk-0.3.7/PKG-INFO → openquantum_sdk-0.4.0/README.md +15 -20
  3. openquantum_sdk-0.4.0/openquantum_sdk/__init__.py +21 -0
  4. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/__main__.py +17 -2
  5. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/backends.py +18 -0
  6. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/clients.py +62 -3
  7. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/models.py +93 -2
  8. openquantum_sdk-0.3.7/README.md → openquantum_sdk-0.4.0/openquantum_sdk.egg-info/PKG-INFO +30 -5
  9. openquantum_sdk-0.4.0/openquantum_sdk.egg-info/requires.txt +7 -0
  10. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/pyproject.toml +3 -3
  11. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/tests/test_backends.py +17 -0
  12. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/tests/test_clients.py +103 -0
  13. openquantum_sdk-0.3.7/openquantum_sdk/__init__.py +0 -5
  14. openquantum_sdk-0.3.7/openquantum_sdk.egg-info/requires.txt +0 -7
  15. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/MANIFEST.in +0 -0
  16. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/auth.py +0 -0
  17. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/enums.py +0 -0
  18. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/qiskit/__init__.py +0 -0
  19. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk/utils.py +0 -0
  20. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/SOURCES.txt +0 -0
  21. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/dependency_links.txt +0 -0
  22. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/entry_points.txt +0 -0
  23. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/openquantum_sdk.egg-info/top_level.txt +0 -0
  24. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/requirements-dev.txt +0 -0
  25. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/requirements.txt +0 -0
  26. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/setup.cfg +0 -0
  27. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/tests/__init__.py +0 -0
  28. {openquantum_sdk-0.3.7 → openquantum_sdk-0.4.0}/tests/test_clients_plan_priority.py +0 -0
  29. {openquantum_sdk-0.3.7 → 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.7
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
@@ -9,9 +9,9 @@ Requires-Python: >=3.8
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: requests>=2.25.0
11
11
  Provides-Extra: qiskit
12
- Requires-Dist: openquantum-sdk-qiskit>=0.2.4; extra == "qiskit"
12
+ Requires-Dist: openquantum-sdk-qiskit>=0.3.0; extra == "qiskit"
13
13
  Provides-Extra: pennylane
14
- Requires-Dist: openquantum-sdk-pennylane>=0.1.0; extra == "pennylane"
14
+ Requires-Dist: openquantum-sdk-pennylane>=0.2.0; extra == "pennylane"
15
15
 
16
16
  # Open Quantum Python SDK
17
17
 
@@ -21,9 +21,11 @@ A lightweight, modern Python SDK for the **Open Quantum Platform**.
21
21
 
22
22
  * **Management API** — list organizations, backend classes, and providers
23
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
24
25
  * **Smart defaults** — auto-select execution plan & queue priority from quote
25
26
  * **Thread-safe auth** — Keycloak client-credentials with auto-refresh
26
27
  * **Automatic output download** — `download_job_output()` returns parsed JSON
28
+ * **Calibration download** — `download_job_calibration()` when the job has `calibration_data_url`
27
29
 
28
30
  ## Installation
29
31
 
@@ -39,7 +41,7 @@ Python 3.9+ required.
39
41
 
40
42
  ### Option 1: SDK Key File (Recommended)
41
43
 
42
- Download from [Open Quantum SDK Keys](https://openquantum.com/keys):
44
+ Download from [Open Quantum SDK Keys](https://www.openquantum.com/keys):
43
45
 
44
46
  ```json
45
47
  {
@@ -130,6 +132,11 @@ try:
130
132
  else:
131
133
  print("No output available.")
132
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
+
133
140
  except Exception as e:
134
141
  print(f"\n--- JOB FAILED ---")
135
142
  print(f"Error: {e}")
@@ -273,6 +280,8 @@ finally:
273
280
  | `queue_priority` | `QueuePriorityType \| "auto"` | `"auto"` | |
274
281
  | `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
275
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 |
276
285
 
277
286
  ## API Reference
278
287
 
@@ -291,6 +300,7 @@ finally:
291
300
  | `cancel_job(job_id)` | `None` | Cancel running job |
292
301
  | `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
293
302
  | **`download_job_output(job)`** | `Any` | **Downloads + parses JSON output** |
303
+ | **`download_job_calibration(job)`** | `Any` | **Downloads + parses calibration JSON** (when tagged) |
294
304
  | `submit_job(config, file_path=... or file_content=...)` | `JobRead` | **One-call submission + polling** |
295
305
 
296
306
  ### `ManagementClient`
@@ -308,15 +318,15 @@ finally:
308
318
  | Model | Key Fields |
309
319
  |-------|------------|
310
320
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
311
- | `ErrorResponse` | `status_code`, `message: List[str]`, `error_code` |
321
+ | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
312
322
  | `JobCategoryRead` | `id`, `name`, `short_code` |
313
323
  | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
314
- | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `credits_used`, `submitted_at` |
315
- | `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` |
316
326
  | `JobPreparationResponse` | `id` |
317
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` |
318
328
  | `JobPreparationUploadResponse` | `id`, `url` |
319
- | `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` |
320
330
  | `OrganizationRead` | `id`, `name` |
321
331
  | `PaginatedBackendClasses` | `backend_classes: List[BackendClassRead]`, `pagination: PaginationInfo` |
322
332
  | `PaginatedJobCategories` | `categories: List[JobCategoryRead]`, `pagination: PaginationInfo` |
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: openquantum-sdk
3
- Version: 0.3.7
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: qiskit
12
- Requires-Dist: openquantum-sdk-qiskit>=0.2.4; extra == "qiskit"
13
- Provides-Extra: pennylane
14
- Requires-Dist: openquantum-sdk-pennylane>=0.1.0; extra == "pennylane"
15
-
16
1
  # Open Quantum Python SDK
17
2
 
18
3
  A lightweight, modern Python SDK for the **Open Quantum Platform**.
@@ -21,9 +6,11 @@ A lightweight, modern Python SDK for the **Open Quantum Platform**.
21
6
 
22
7
  * **Management API** — list organizations, backend classes, and providers
23
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
24
10
  * **Smart defaults** — auto-select execution plan & queue priority from quote
25
11
  * **Thread-safe auth** — Keycloak client-credentials with auto-refresh
26
12
  * **Automatic output download** — `download_job_output()` returns parsed JSON
13
+ * **Calibration download** — `download_job_calibration()` when the job has `calibration_data_url`
27
14
 
28
15
  ## Installation
29
16
 
@@ -39,7 +26,7 @@ Python 3.9+ required.
39
26
 
40
27
  ### Option 1: SDK Key File (Recommended)
41
28
 
42
- Download from [Open Quantum SDK Keys](https://openquantum.com/keys):
29
+ Download from [Open Quantum SDK Keys](https://www.openquantum.com/keys):
43
30
 
44
31
  ```json
45
32
  {
@@ -130,6 +117,11 @@ try:
130
117
  else:
131
118
  print("No output available.")
132
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
+
133
125
  except Exception as e:
134
126
  print(f"\n--- JOB FAILED ---")
135
127
  print(f"Error: {e}")
@@ -273,6 +265,8 @@ finally:
273
265
  | `queue_priority` | `QueuePriorityType \| "auto"` | `"auto"` | |
274
266
  | `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
275
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 |
276
270
 
277
271
  ## API Reference
278
272
 
@@ -291,6 +285,7 @@ finally:
291
285
  | `cancel_job(job_id)` | `None` | Cancel running job |
292
286
  | `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
293
287
  | **`download_job_output(job)`** | `Any` | **Downloads + parses JSON output** |
288
+ | **`download_job_calibration(job)`** | `Any` | **Downloads + parses calibration JSON** (when tagged) |
294
289
  | `submit_job(config, file_path=... or file_content=...)` | `JobRead` | **One-call submission + polling** |
295
290
 
296
291
  ### `ManagementClient`
@@ -308,15 +303,15 @@ finally:
308
303
  | Model | Key Fields |
309
304
  |-------|------------|
310
305
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
311
- | `ErrorResponse` | `status_code`, `message: List[str]`, `error_code` |
306
+ | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
312
307
  | `JobCategoryRead` | `id`, `name`, `short_code` |
313
308
  | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
314
- | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `credits_used`, `submitted_at` |
315
- | `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` |
316
311
  | `JobPreparationResponse` | `id` |
317
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` |
318
313
  | `JobPreparationUploadResponse` | `id`, `url` |
319
- | `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` |
320
315
  | `OrganizationRead` | `id`, `name` |
321
316
  | `PaginatedBackendClasses` | `backend_classes: List[BackendClassRead]`, `pagination: PaginationInfo` |
322
317
  | `PaginatedJobCategories` | `categories: List[JobCategoryRead]`, `pagination: PaginationInfo` |
@@ -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:
@@ -163,12 +164,13 @@ def main():
163
164
  print(f"Final Status: {job.status}")
164
165
  print(f"Submitted At: {job.submitted_at}")
165
166
  print(f"Output URL: {job.output_data_url}")
167
+ print(f"Calibration URL: {job.calibration_data_url}")
166
168
  print("---------------------")
167
169
 
168
170
  if job.output_data_url:
169
171
  print("\nFetching job output...")
170
172
  try:
171
- output_json = scheduler.download_job_output(job.id)
173
+ output_json = scheduler.download_job_output(job)
172
174
  print("\n--- JOB OUTPUT (JSON) ---")
173
175
  print(json.dumps(output_json, indent=2))
174
176
  print("-------------------------")
@@ -177,6 +179,19 @@ def main():
177
179
  else:
178
180
  print("\nNo output_data_url available (job may have failed or produced no output).")
179
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)
180
195
  except requests.exceptions.HTTPError as e:
181
196
  print(f"API error: {e.response.status_code} - {e.response.text}", file=sys.stderr)
182
197
  sys.exit(1)
@@ -67,6 +67,9 @@ class BackendCapabilities:
67
67
  "verbatim_boxes", ...). See sdk/docs/device-features.md and
68
68
  the DeviceFeatures enum. These drive precompilation,
69
69
  validation, and SDK-side decisions before jobs reach QPUs.
70
+ supported_formats: Circuit input language families this backend
71
+ accepts (e.g. ``"qasm"``, ``"qblox_sequence"``). Defaults to
72
+ ``["qasm"]``. OpenQASM 2 and 3 both map to ``"qasm"``.
70
73
  """
71
74
 
72
75
  short_code: str
@@ -76,6 +79,7 @@ class BackendCapabilities:
76
79
  topology: Topology = field(default_factory=Topology)
77
80
  limits: Dict[str, Any] = field(default_factory=dict)
78
81
  features: List[str] = field(default_factory=list)
82
+ supported_formats: List[str] = field(default_factory=lambda: ["qasm"])
79
83
 
80
84
  @property
81
85
  def supported_gate_names(self) -> set[str]:
@@ -136,6 +140,11 @@ class BackendCapabilities:
136
140
  """True if classical control flow (if/for/while on classical bits) is supported."""
137
141
  return self.supports_feature("classical_control")
138
142
 
143
+ def supports_format(self, name: str) -> bool:
144
+ """Return True if the backend accepts the given input format family."""
145
+ formats = self.supported_formats or ["qasm"]
146
+ return name in formats
147
+
139
148
  def to_dict(self) -> Dict[str, Any]:
140
149
  """Convert to the dict format used by the Qiskit plugin's target builder."""
141
150
  return {
@@ -148,6 +157,9 @@ class BackendCapabilities:
148
157
  "noise": {},
149
158
  "limits": self.limits,
150
159
  "features": list(self.features) if self.features else ["measure"],
160
+ "supported_formats": list(self.supported_formats)
161
+ if self.supported_formats
162
+ else ["qasm"],
151
163
  }
152
164
 
153
165
 
@@ -624,6 +636,11 @@ def capabilities_from_constraint_data(
624
636
  directed_edges=topo_data.get("directed_edges", False),
625
637
  coupling_map=topo_data.get("coupling_map", []),
626
638
  )
639
+ raw_formats = constraint_data.get("supported_formats") or []
640
+ supported_formats = (
641
+ [str(f) for f in raw_formats] if raw_formats else ["qasm"]
642
+ )
643
+
627
644
  return BackendCapabilities(
628
645
  short_code=short_code,
629
646
  n_qubits=constraint_data.get("n_qubits", 0),
@@ -632,6 +649,7 @@ def capabilities_from_constraint_data(
632
649
  topology=topology,
633
650
  limits=constraint_data.get("limits", {}),
634
651
  features=constraint_data.get("features", []) or [],
652
+ supported_formats=supported_formats,
635
653
  )
636
654
 
637
655
 
@@ -11,6 +11,9 @@ from .enums import AutoChoice, ExecutionPlanType, QueuePriorityType
11
11
  from .utils import strip_gate_definitions
12
12
  from .models import (
13
13
  APIError,
14
+ TermsOfUseRequiredError,
15
+ error_from_response,
16
+ is_not_found_error,
14
17
  BackendClassRead,
15
18
  CreditBalanceRead,
16
19
  ErrorResponse,
@@ -66,6 +69,12 @@ class JobSubmissionConfig:
66
69
  submission. Useful in notebooks and CLI scripts.
67
70
  job_timeout_seconds: Maximum seconds to wait for the job to complete.
68
71
  Defaults to 86 400 (1 day).
72
+ submitted_with: Client/surface that submitted this preparation
73
+ (e.g. ``"sdk"``, ``"qiskit"``, ``"cli"``). Defaults to ``"sdk"``.
74
+ input_format: Circuit input language family (``"qasm"`` or
75
+ ``"qblox_sequence"``). Defaults to ``"qasm"``. OpenQASM 2/3
76
+ both use ``"qasm"``; use ``"qblox_sequence"`` for Qblox
77
+ sequencer JSON (program + waveforms).
69
78
 
70
79
  Example:
71
80
  >>> config = JobSubmissionConfig(
@@ -93,6 +102,12 @@ class JobSubmissionConfig:
93
102
  # USER-CONTROLLABLE: How long to wait for job to finish
94
103
  job_timeout_seconds: int = 86_400 # 1 day
95
104
 
105
+ # Attribution for analytics / provenance
106
+ submitted_with: Optional[str] = "sdk"
107
+
108
+ # Circuit language family (qasm | qblox_sequence). None → API default "qasm".
109
+ input_format: Optional[str] = None
110
+
96
111
  # INTERNAL: Fixed preparation timeout and polling
97
112
  _preparation_timeout_seconds: int = 300 # 5 minutes
98
113
  _preparation_poll_interval: float = 2.0
@@ -159,9 +174,12 @@ class BaseClient:
159
174
  err = ErrorResponse(
160
175
  status_code=payload.get("status_code", resp.status_code),
161
176
  message=payload.get("message", []),
177
+ type=payload.get("type"),
162
178
  error_code=payload.get("error_code"),
163
179
  )
164
- raise APIError(err)
180
+ raise error_from_response(err)
181
+ except (TermsOfUseRequiredError, APIError):
182
+ raise
165
183
  except ValueError:
166
184
  pass
167
185
  resp.raise_for_status()
@@ -422,6 +440,39 @@ class SchedulerClient(BaseClient):
422
440
  "Content may not be JSON or is malformed."
423
441
  ) from exc
424
442
 
443
+ def download_job_calibration(self, job: JobRead) -> Any:
444
+ """Download and parse the calibration snapshot tagged on a job.
445
+
446
+ Calibration is optional: only present if reported.
447
+
448
+ Args:
449
+ job: A ``JobRead`` from ``get_job`` / ``submit_job``. Must have a
450
+ non-``None`` ``calibration_data_url``.
451
+
452
+ Returns:
453
+ Parsed JSON calibration payload.
454
+
455
+ Raises:
456
+ RuntimeError: If the job has no ``calibration_data_url``.
457
+ ValueError: If the body cannot be parsed as JSON.
458
+ """
459
+ if not job.calibration_data_url:
460
+ raise RuntimeError(
461
+ f"Job {job.id} has no calibration_data_url. "
462
+ "No calibration was tagged on the completing execution."
463
+ )
464
+
465
+ resp = requests.get(job.calibration_data_url)
466
+ resp.raise_for_status()
467
+
468
+ try:
469
+ return resp.json()
470
+ except ValueError as exc:
471
+ raise ValueError(
472
+ f"Failed to parse calibration of job {job.id} as JSON. "
473
+ "Content may not be JSON or is malformed."
474
+ ) from exc
475
+
425
476
  def prepare_job(self, preparation: JobPreparationCreate) -> JobPreparationResponse:
426
477
  """Submit a job preparation request.
427
478
 
@@ -435,7 +486,12 @@ class SchedulerClient(BaseClient):
435
486
  Returns:
436
487
  A ``JobPreparationResponse`` containing the preparation ``id``.
437
488
  """
438
- data = self._request("POST", "/v1/jobs/prepare", json=asdict(preparation))
489
+ payload = {
490
+ key: value
491
+ for key, value in asdict(preparation).items()
492
+ if value is not None
493
+ }
494
+ data = self._request("POST", "/v1/jobs/prepare", json=payload)
439
495
  return JobPreparationResponse.from_dict(data)
440
496
 
441
497
  def get_preparation_result(self, preparation_id: str) -> JobPreparationResultResponse:
@@ -496,6 +552,7 @@ class SchedulerClient(BaseClient):
496
552
  message=data.get("message"),
497
553
  quote=quote,
498
554
  configuration_data=data.get("configuration_data", {}),
555
+ input_format=data.get("input_format"),
499
556
  )
500
557
 
501
558
  def create_job(self, job: JobCreate) -> JobRead:
@@ -651,6 +708,8 @@ class SchedulerClient(BaseClient):
651
708
  job_subcategory_id=config.job_subcategory_id,
652
709
  shots=config.shots,
653
710
  configuration_data=config.configuration_data or {},
711
+ submitted_with=config.submitted_with,
712
+ input_format=config.input_format,
654
713
  )
655
714
  if config.verbose:
656
715
  print("Step 2/5: Preparing job and fetching quote...")
@@ -749,7 +808,7 @@ class SchedulerClient(BaseClient):
749
808
  done = resp.status in ("Completed", "Failed")
750
809
  return done, resp
751
810
  except APIError as e:
752
- if e.error_response.status_code == 404:
811
+ if is_not_found_error(e):
753
812
  if verbose:
754
813
  print(" > Preparation status: Not found (retrying...)")
755
814
  return False, None
@@ -50,6 +50,24 @@ class PaginationInfo(FlexibleDataclass):
50
50
  extra: Dict[str, Any] = field(default_factory=dict, repr=False)
51
51
 
52
52
 
53
+ class ApiErrorType:
54
+ UNAUTHORIZED = "UNAUTHORIZED"
55
+ FORBIDDEN = "FORBIDDEN"
56
+ INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS"
57
+ ORG_MEMBERSHIP_REQUIRED = "ORG_MEMBERSHIP_REQUIRED"
58
+ TERMS_OF_USE_REQUIRED = "TERMS_OF_USE_REQUIRED"
59
+ NOT_FOUND = "NOT_FOUND"
60
+ CONFLICT = "CONFLICT"
61
+ VALIDATION_ERROR = "VALIDATION_ERROR"
62
+ BAD_REQUEST = "BAD_REQUEST"
63
+ INSUFFICIENT_CREDITS = "INSUFFICIENT_CREDITS"
64
+ JOB_LIMIT_REACHED = "JOB_LIMIT_REACHED"
65
+ JOB_NOT_CANCELABLE = "JOB_NOT_CANCELABLE"
66
+ RESOURCE_NOT_ACTIVE = "RESOURCE_NOT_ACTIVE"
67
+ NOT_READY = "NOT_READY"
68
+ INTERNAL_ERROR = "INTERNAL_ERROR"
69
+
70
+
53
71
  @dataclass
54
72
  class ErrorResponse:
55
73
  """Structured error payload returned by the API on HTTP 4xx/5xx responses.
@@ -57,11 +75,13 @@ class ErrorResponse:
57
75
  Attributes:
58
76
  status_code: The HTTP status code from the response.
59
77
  message: List of human-readable error message strings.
60
- error_code: Optional request/error identifier for support tracing.
78
+ type: Stable machine-readable error type (see ``ApiErrorType``).
79
+ error_code: Request identifier for support tracing.
61
80
  """
62
81
 
63
82
  status_code: int
64
83
  message: Union[List[str], str]
84
+ type: Optional[str] = None
65
85
  error_code: Optional[str] = None
66
86
 
67
87
 
@@ -79,7 +99,59 @@ class APIError(Exception):
79
99
  self.error_response = error_response
80
100
  msgs = error_response.message if isinstance(error_response.message, list) else [error_response.message]
81
101
  msg = "; ".join(msgs)
82
- super().__init__(f"API Error {error_response.status_code}: {msg} (Request ID: {error_response.error_code})")
102
+ type_part = f" [{error_response.type}]" if error_response.type else ""
103
+ super().__init__(
104
+ f"API Error {error_response.status_code}{type_part}: {msg} "
105
+ f"(Request ID: {error_response.error_code})"
106
+ )
107
+
108
+
109
+ TERMS_OF_USE_REQUIRED_MESSAGE = (
110
+ "Please visit https://www.openquantum.com to accept the Terms of Use before submitting jobs."
111
+ )
112
+
113
+
114
+ class TermsOfUseRequiredError(APIError):
115
+ """Raised when job prepare/create is blocked until Terms of Use are accepted.
116
+
117
+ Users must visit https://www.openquantum.com, sign in, and accept the latest
118
+ Terms of Use (and complete portal onboarding) before submitting jobs.
119
+ """
120
+
121
+ def __init__(self, error_response: Optional[ErrorResponse] = None):
122
+ if error_response is None:
123
+ error_response = ErrorResponse(
124
+ status_code=403,
125
+ message=TERMS_OF_USE_REQUIRED_MESSAGE,
126
+ type=ApiErrorType.TERMS_OF_USE_REQUIRED,
127
+ )
128
+ # Keep error_response for callers that inspect status/request id, but
129
+ # present a clean, actionable message (not the generic API Error wrapper).
130
+ self.error_response = error_response
131
+ Exception.__init__(self, TERMS_OF_USE_REQUIRED_MESSAGE)
132
+
133
+
134
+ def error_from_response(error_response: ErrorResponse) -> APIError:
135
+ """Map an API ``ErrorResponse`` to the most specific SDK exception.
136
+
137
+ Branching uses the stable ``type`` field only — never message text.
138
+ """
139
+ if error_response.type == ApiErrorType.TERMS_OF_USE_REQUIRED:
140
+ return TermsOfUseRequiredError(error_response)
141
+ return APIError(error_response)
142
+
143
+
144
+ def is_not_found_error(error: APIError) -> bool:
145
+ """Return True if the error indicates a missing resource.
146
+
147
+ Prefers ``ErrorResponse.type``; falls back to HTTP 404 when ``type``
148
+ is absent (e.g. older gateways).
149
+ """
150
+ if error.error_response.type == ApiErrorType.NOT_FOUND:
151
+ return True
152
+ if error.error_response.type is None and error.error_response.status_code == 404:
153
+ return True
154
+ return False
83
155
 
84
156
 
85
157
  # ---------------------------------------------------------------------------
@@ -253,6 +325,14 @@ class JobPreparationCreate:
253
325
  job_subcategory_id: Job subcategory UUID or short code.
254
326
  shots: Number of measurement shots to execute.
255
327
  configuration_data: Optional backend-specific configuration.
328
+ submitted_with: Client/surface that submitted this preparation
329
+ (e.g. ``"sdk"``, ``"qiskit"``, ``"pennylane"``, ``"cli"``).
330
+ Optional; omit or ``None`` for unknown/legacy clients.
331
+ input_format: Circuit input language family (``"qasm"`` or
332
+ ``"qblox_sequence"``). Defaults to ``"qasm"``. OpenQASM 2 and
333
+ 3 both use ``"qasm"``; version is detected from the file.
334
+ ``"qblox_sequence"`` is Qblox sequencer JSON. Must be listed
335
+ in the backend's ``constraint_data.supported_formats``.
256
336
  """
257
337
 
258
338
  organization_id: str
@@ -262,6 +342,8 @@ class JobPreparationCreate:
262
342
  job_subcategory_id: str
263
343
  shots: int
264
344
  configuration_data: Dict[str, Any] = field(default_factory=dict)
345
+ submitted_with: Optional[str] = None
346
+ input_format: Optional[str] = None
265
347
 
266
348
 
267
349
  @dataclass
@@ -355,6 +437,8 @@ class JobPreparationResultResponse:
355
437
  quote: List of available execution plans with pricing details.
356
438
  message: Optional status or error message.
357
439
  configuration_data: Backend-specific configuration echoed back.
440
+ input_format: Circuit input language family (``"qasm"`` or
441
+ ``"qblox_sequence"``).
358
442
  extra: Additional fields returned by the API not mapped to attributes.
359
443
  """
360
444
 
@@ -369,6 +453,7 @@ class JobPreparationResultResponse:
369
453
  quote: List[QuotePlan]
370
454
  message: Optional[str] = None
371
455
  configuration_data: Dict[str, Any] = field(default_factory=dict)
456
+ input_format: Optional[str] = None
372
457
  extra: Dict[str, Any] = field(default_factory=dict, repr=False)
373
458
 
374
459
 
@@ -406,6 +491,8 @@ class JobRead(FlexibleDataclass):
406
491
  message: Optional status or error message from the backend.
407
492
  output_data_url: URL to download results from, available once
408
493
  the job completes.
494
+ calibration_data_url: Presigned URL for the calibration snapshot
495
+ tagged on the successful execution, if any.
409
496
  transaction_id: Credit transaction ID, if a charge was applied.
410
497
  submitted_at: ISO 8601 timestamp of when the job was submitted.
411
498
  extra: Additional fields returned by the API not mapped to attributes.
@@ -419,6 +506,7 @@ class JobRead(FlexibleDataclass):
419
506
  queue_priority_id: str
420
507
  message: Optional[str] = None
421
508
  output_data_url: Optional[str] = None
509
+ calibration_data_url: Optional[str] = None
422
510
  transaction_id: Optional[str] = None
423
511
  submitted_at: Optional[str] = None
424
512
  extra: Dict[str, Any] = field(default_factory=dict, repr=False)
@@ -438,6 +526,8 @@ class JobList(FlexibleDataclass):
438
526
  status: Current job status string.
439
527
  backend_name: Display name of the backend, if available.
440
528
  output_data_url: URL to download results, if the job completed.
529
+ calibration_data_url: Presigned URL for the calibration snapshot
530
+ tagged on the successful execution, if any.
441
531
  credits_used: Number of credits charged for this job.
442
532
  submitted_at: ISO 8601 timestamp of when the job was submitted.
443
533
  extra: Additional fields returned by the API not mapped to attributes.
@@ -449,6 +539,7 @@ class JobList(FlexibleDataclass):
449
539
  status: str
450
540
  backend_name: Optional[str] = None
451
541
  output_data_url: Optional[str] = None
542
+ calibration_data_url: Optional[str] = None
452
543
  credits_used: Optional[float] = None
453
544
  submitted_at: Optional[str] = None
454
545
  extra: Dict[str, Any] = field(default_factory=dict, repr=False)
@@ -1,3 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: openquantum-sdk
3
+ Version: 0.4.0
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: 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"
15
+
1
16
  # Open Quantum Python SDK
2
17
 
3
18
  A lightweight, modern Python SDK for the **Open Quantum Platform**.
@@ -6,9 +21,11 @@ A lightweight, modern Python SDK for the **Open Quantum Platform**.
6
21
 
7
22
  * **Management API** — list organizations, backend classes, and providers
8
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
9
25
  * **Smart defaults** — auto-select execution plan & queue priority from quote
10
26
  * **Thread-safe auth** — Keycloak client-credentials with auto-refresh
11
27
  * **Automatic output download** — `download_job_output()` returns parsed JSON
28
+ * **Calibration download** — `download_job_calibration()` when the job has `calibration_data_url`
12
29
 
13
30
  ## Installation
14
31
 
@@ -24,7 +41,7 @@ Python 3.9+ required.
24
41
 
25
42
  ### Option 1: SDK Key File (Recommended)
26
43
 
27
- Download from [Open Quantum SDK Keys](https://openquantum.com/keys):
44
+ Download from [Open Quantum SDK Keys](https://www.openquantum.com/keys):
28
45
 
29
46
  ```json
30
47
  {
@@ -115,6 +132,11 @@ try:
115
132
  else:
116
133
  print("No output available.")
117
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
+
118
140
  except Exception as e:
119
141
  print(f"\n--- JOB FAILED ---")
120
142
  print(f"Error: {e}")
@@ -258,6 +280,8 @@ finally:
258
280
  | `queue_priority` | `QueuePriorityType \| "auto"` | `"auto"` | |
259
281
  | `auto_approve_quote` | `bool` | `True` | Set `False` in notebooks |
260
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 |
261
285
 
262
286
  ## API Reference
263
287
 
@@ -276,6 +300,7 @@ finally:
276
300
  | `cancel_job(job_id)` | `None` | Cancel running job |
277
301
  | `list_jobs(organization_id, limit=20, cursor=..., status=...)` | `PaginatedJobs` | List your jobs |
278
302
  | **`download_job_output(job)`** | `Any` | **Downloads + parses JSON output** |
303
+ | **`download_job_calibration(job)`** | `Any` | **Downloads + parses calibration JSON** (when tagged) |
279
304
  | `submit_job(config, file_path=... or file_content=...)` | `JobRead` | **One-call submission + polling** |
280
305
 
281
306
  ### `ManagementClient`
@@ -293,15 +318,15 @@ finally:
293
318
  | Model | Key Fields |
294
319
  |-------|------------|
295
320
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
296
- | `ErrorResponse` | `status_code`, `message: List[str]`, `error_code` |
321
+ | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
297
322
  | `JobCategoryRead` | `id`, `name`, `short_code` |
298
323
  | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
299
- | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `credits_used`, `submitted_at` |
300
- | `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` |
301
326
  | `JobPreparationResponse` | `id` |
302
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` |
303
328
  | `JobPreparationUploadResponse` | `id`, `url` |
304
- | `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` |
305
330
  | `OrganizationRead` | `id`, `name` |
306
331
  | `PaginatedBackendClasses` | `backend_classes: List[BackendClassRead]`, `pagination: PaginationInfo` |
307
332
  | `PaginatedJobCategories` | `categories: List[JobCategoryRead]`, `pagination: PaginationInfo` |
@@ -0,0 +1,7 @@
1
+ requests>=2.25.0
2
+
3
+ [pennylane]
4
+ openquantum-sdk-pennylane>=0.2.0
5
+
6
+ [qiskit]
7
+ openquantum-sdk-qiskit>=0.3.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openquantum-sdk"
7
- version = "0.3.7"
7
+ version = "0.4.0"
8
8
  description = "Python SDK for Open Quantum"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -19,8 +19,8 @@ classifiers = [
19
19
  ]
20
20
 
21
21
  [project.optional-dependencies]
22
- qiskit = ["openquantum-sdk-qiskit>=0.2.4"]
23
- pennylane = ["openquantum-sdk-pennylane>=0.1.0"]
22
+ qiskit = ["openquantum-sdk-qiskit>=0.3.0"]
23
+ pennylane = ["openquantum-sdk-pennylane>=0.2.0"]
24
24
 
25
25
  [project.scripts]
26
26
  openquantum-sdk = "openquantum_sdk.__main__:main"
@@ -189,6 +189,23 @@ class TestBackendFeatures:
189
189
  assert caps.allows_barriers is True
190
190
  assert caps.allows_reset is False
191
191
  assert caps.allows_dynamic_circuits is False
192
+ # Missing supported_formats defaults to qasm
193
+ assert caps.supported_formats == ["qasm"]
194
+ assert caps.supports_format("qasm") is True
195
+ assert caps.supports_format("qblox_sequence") is False
196
+
197
+ def test_capabilities_from_constraint_data_parses_supported_formats(self):
198
+ cd = {
199
+ "n_qubits": 8,
200
+ "native_ops": [],
201
+ "topology": {"directed_edges": False, "coupling_map": []},
202
+ "supported_formats": ["qblox_sequence"],
203
+ }
204
+ caps = capabilities_from_constraint_data("test:qblox", cd)
205
+ assert caps.supported_formats == ["qblox_sequence"]
206
+ assert caps.supports_format("qblox_sequence") is True
207
+ assert caps.supports_format("qasm") is False
208
+ assert caps.to_dict()["supported_formats"] == ["qblox_sequence"]
192
209
 
193
210
  def test_features_helpers_on_live_style_data(self):
194
211
  cd = {
@@ -13,10 +13,13 @@ from openquantum_sdk.clients import (
13
13
  )
14
14
  from openquantum_sdk.enums import ExecutionPlanType, QueuePriorityType
15
15
  from openquantum_sdk.models import (
16
+ JobCreate,
16
17
  QuotePlan,
17
18
  QueuePriority,
18
19
  JobPreparationCreate,
19
20
  OrganizationRead,
21
+ TERMS_OF_USE_REQUIRED_MESSAGE,
22
+ TermsOfUseRequiredError,
20
23
  )
21
24
 
22
25
 
@@ -54,6 +57,7 @@ def mock_scheduler():
54
57
  "queue_priority_id": "prio-1",
55
58
  "input_data_url": "https://input.example.com/job-123",
56
59
  "output_data_url": "https://output.example.com/result-123",
60
+ "calibration_data_url": "https://calibration.example.com/cal-123",
57
61
  "transaction_id": "tx-789",
58
62
  "submitted_at": "2025-04-05T12:00:00Z",
59
63
  },
@@ -425,6 +429,26 @@ def test_download_job_output_non_json(mock_scheduler):
425
429
  mock_scheduler.download_job_output(job)
426
430
 
427
431
 
432
+ def test_download_job_calibration_success(mock_scheduler):
433
+ m = mock_scheduler.session.mocker
434
+ m.get(
435
+ "https://calibration.example.com/cal-123",
436
+ json={"classifiers": {}, "components": {}},
437
+ )
438
+ job = mock_scheduler.get_job("job-123")
439
+ # Inject calibration URL as API would after completion
440
+ job.calibration_data_url = "https://calibration.example.com/cal-123"
441
+ cal = mock_scheduler.download_job_calibration(job)
442
+ assert "classifiers" in cal
443
+
444
+
445
+ def test_download_job_calibration_no_url(mock_scheduler):
446
+ job = mock_scheduler.get_job("job-123")
447
+ job.calibration_data_url = None
448
+ with pytest.raises(RuntimeError, match=r"no calibration_data_url"):
449
+ mock_scheduler.download_job_calibration(job)
450
+
451
+
428
452
  def test_download_job_output_http_error(mock_scheduler):
429
453
  job = mock_scheduler.get_job("job-123")
430
454
  mock_scheduler.session.mocker.get(
@@ -436,6 +460,85 @@ def test_download_job_output_http_error(mock_scheduler):
436
460
  mock_scheduler.download_job_output(job)
437
461
 
438
462
 
463
+ def test_prepare_job_terms_of_use_required(mock_scheduler):
464
+ mock_scheduler.session.mocker.post(
465
+ "https://scheduler.openquantum.com/v1/jobs/prepare",
466
+ status_code=403,
467
+ json={
468
+ "status_code": 403,
469
+ "message": [TERMS_OF_USE_REQUIRED_MESSAGE],
470
+ "type": "TERMS_OF_USE_REQUIRED",
471
+ "error_code": "req-terms",
472
+ },
473
+ )
474
+ prep = JobPreparationCreate(
475
+ organization_id="org-123",
476
+ backend_class_id="backend-1",
477
+ name="Test",
478
+ upload_endpoint_id="upload-1",
479
+ job_subcategory_id="sub-1",
480
+ shots=10,
481
+ configuration_data={},
482
+ )
483
+ with pytest.raises(TermsOfUseRequiredError, match="openquantum.com") as exc_info:
484
+ mock_scheduler.prepare_job(prep)
485
+ assert str(exc_info.value) == TERMS_OF_USE_REQUIRED_MESSAGE
486
+ assert exc_info.value.error_response.status_code == 403
487
+ assert exc_info.value.error_response.type == "TERMS_OF_USE_REQUIRED"
488
+
489
+
490
+ def test_create_job_terms_of_use_required(mock_scheduler):
491
+ mock_scheduler.session.mocker.post(
492
+ "https://scheduler.openquantum.com/v1/jobs",
493
+ status_code=403,
494
+ json={
495
+ "status_code": 403,
496
+ "message": TERMS_OF_USE_REQUIRED_MESSAGE,
497
+ "type": "TERMS_OF_USE_REQUIRED",
498
+ "error_code": "req-terms",
499
+ },
500
+ )
501
+ job = JobCreate(
502
+ organization_id="org-123",
503
+ job_preparation_id="prep-1",
504
+ execution_plan_id="plan-1",
505
+ queue_priority_id="prio-1",
506
+ )
507
+ with pytest.raises(TermsOfUseRequiredError, match="Terms of Use"):
508
+ mock_scheduler.create_job(job)
509
+
510
+
511
+ def test_prepare_job_org_membership_required(mock_scheduler):
512
+ from openquantum_sdk.models import APIError, ApiErrorType
513
+
514
+ mock_scheduler.session.mocker.post(
515
+ "https://scheduler.openquantum.com/v1/jobs/prepare",
516
+ status_code=403,
517
+ json={
518
+ "status_code": 403,
519
+ "message": ["User is not a member of the organization"],
520
+ "type": ApiErrorType.ORG_MEMBERSHIP_REQUIRED,
521
+ "error_code": "req-org",
522
+ },
523
+ )
524
+ prep = JobPreparationCreate(
525
+ organization_id="org-123",
526
+ backend_class_id="backend-1",
527
+ name="Test",
528
+ upload_endpoint_id="upload-1",
529
+ job_subcategory_id="sub-1",
530
+ shots=10,
531
+ configuration_data={},
532
+ )
533
+ with pytest.raises(APIError) as exc_info:
534
+ mock_scheduler.prepare_job(prep)
535
+ assert not isinstance(exc_info.value, TermsOfUseRequiredError)
536
+ assert (
537
+ exc_info.value.error_response.type
538
+ == ApiErrorType.ORG_MEMBERSHIP_REQUIRED
539
+ )
540
+
541
+
439
542
  # ---------------------------------------------------------------------------
440
543
  # Categories & Listing
441
544
  # ---------------------------------------------------------------------------
@@ -1,5 +0,0 @@
1
- from .clients import SchedulerClient, ManagementClient
2
- from .utils import poll_for_status
3
-
4
- __version__ = "0.3.7"
5
- __all__ = ["SchedulerClient", "ManagementClient", "poll_for_status"]
@@ -1,7 +0,0 @@
1
- requests>=2.25.0
2
-
3
- [pennylane]
4
- openquantum-sdk-pennylane>=0.1.0
5
-
6
- [qiskit]
7
- openquantum-sdk-qiskit>=0.2.4