openquantum-sdk 0.4.0__tar.gz → 0.4.2__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 (28) hide show
  1. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/PKG-INFO +13 -7
  2. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/README.md +3 -4
  3. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/__init__.py +5 -1
  4. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/__main__.py +8 -3
  5. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/backends.py +2 -2
  6. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/clients.py +53 -15
  7. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/models.py +43 -13
  8. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/PKG-INFO +13 -7
  9. openquantum_sdk-0.4.2/openquantum_sdk.egg-info/requires.txt +15 -0
  10. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/pyproject.toml +12 -5
  11. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/requirements.txt +0 -1
  12. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/tests/test_clients.py +52 -1
  13. openquantum_sdk-0.4.0/openquantum_sdk.egg-info/requires.txt +0 -7
  14. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/MANIFEST.in +0 -0
  15. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/auth.py +0 -0
  16. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/enums.py +0 -0
  17. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/qiskit/__init__.py +0 -0
  18. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk/utils.py +0 -0
  19. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/SOURCES.txt +0 -0
  20. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/dependency_links.txt +0 -0
  21. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/entry_points.txt +0 -0
  22. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/top_level.txt +0 -0
  23. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/requirements-dev.txt +0 -0
  24. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/setup.cfg +0 -0
  25. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/tests/__init__.py +0 -0
  26. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/tests/test_backends.py +0 -0
  27. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/tests/test_clients_plan_priority.py +0 -0
  28. {openquantum_sdk-0.4.0 → openquantum_sdk-0.4.2}/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.4.0
3
+ Version: 0.4.2
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,10 +8,17 @@ 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: 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"
11
18
  Provides-Extra: qiskit
12
- Requires-Dist: openquantum-sdk-qiskit>=0.3.0; extra == "qiskit"
19
+ Requires-Dist: openquantum-sdk-qiskit>=0.3.1; extra == "qiskit"
13
20
  Provides-Extra: pennylane
14
- Requires-Dist: openquantum-sdk-pennylane>=0.2.0; extra == "pennylane"
21
+ Requires-Dist: openquantum-sdk-pennylane>=0.2.1; extra == "pennylane"
15
22
 
16
23
  # Open Quantum Python SDK
17
24
 
@@ -35,7 +42,7 @@ pip install -U openquantum-sdk
35
42
  pip install -e .
36
43
  ```
37
44
 
38
- Python 3.9+ required.
45
+ Python 3.8+ required.
39
46
 
40
47
  ## Authentication
41
48
 
@@ -220,9 +227,8 @@ try:
220
227
  cheapest_plan = min(result.quote, key=lambda p: p.price)
221
228
  cheapest_prio = min(cheapest_plan.queue_priorities, key=lambda q: q.price_increase)
222
229
 
223
- # 7. Create job
230
+ # 7. Create job (organization comes from the preparation)
224
231
  job = JobCreate(
225
- organization_id=org.id,
226
232
  job_preparation_id=prep_resp.id,
227
233
  execution_plan_id=cheapest_plan.execution_plan_id,
228
234
  queue_priority_id=cheapest_prio.queue_priority_id,
@@ -320,7 +326,7 @@ finally:
320
326
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
321
327
  | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
322
328
  | `JobCategoryRead` | `id`, `name`, `short_code` |
323
- | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
329
+ | `JobCreate` | `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, optional `organization_id` |
324
330
  | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
325
331
  | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
326
332
  | `JobPreparationResponse` | `id` |
@@ -20,7 +20,7 @@ pip install -U openquantum-sdk
20
20
  pip install -e .
21
21
  ```
22
22
 
23
- Python 3.9+ required.
23
+ Python 3.8+ required.
24
24
 
25
25
  ## Authentication
26
26
 
@@ -205,9 +205,8 @@ try:
205
205
  cheapest_plan = min(result.quote, key=lambda p: p.price)
206
206
  cheapest_prio = min(cheapest_plan.queue_priorities, key=lambda q: q.price_increase)
207
207
 
208
- # 7. Create job
208
+ # 7. Create job (organization comes from the preparation)
209
209
  job = JobCreate(
210
- organization_id=org.id,
211
210
  job_preparation_id=prep_resp.id,
212
211
  execution_plan_id=cheapest_plan.execution_plan_id,
213
212
  queue_priority_id=cheapest_prio.queue_priority_id,
@@ -305,7 +304,7 @@ finally:
305
304
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
306
305
  | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
307
306
  | `JobCategoryRead` | `id`, `name`, `short_code` |
308
- | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
307
+ | `JobCreate` | `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, optional `organization_id` |
309
308
  | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
310
309
  | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
311
310
  | `JobPreparationResponse` | `id` |
@@ -4,11 +4,13 @@ from .models import (
4
4
  ApiErrorType,
5
5
  TermsOfUseRequiredError,
6
6
  TERMS_OF_USE_REQUIRED_MESSAGE,
7
+ EmailVerificationRequiredError,
8
+ EMAIL_VERIFICATION_REQUIRED_MESSAGE,
7
9
  is_not_found_error,
8
10
  )
9
11
  from .utils import poll_for_status
10
12
 
11
- __version__ = "0.4.0"
13
+ __version__ = "0.4.2"
12
14
  __all__ = [
13
15
  "SchedulerClient",
14
16
  "ManagementClient",
@@ -17,5 +19,7 @@ __all__ = [
17
19
  "ApiErrorType",
18
20
  "TermsOfUseRequiredError",
19
21
  "TERMS_OF_USE_REQUIRED_MESSAGE",
22
+ "EmailVerificationRequiredError",
23
+ "EMAIL_VERIFICATION_REQUIRED_MESSAGE",
20
24
  "is_not_found_error",
21
25
  ]
@@ -7,7 +7,7 @@ import argparse
7
7
  import requests
8
8
  from .auth import ClientCredentials, ClientCredentialsAuth
9
9
  from .clients import SchedulerClient, ManagementClient, JobSubmissionConfig
10
- from .models import APIError, TermsOfUseRequiredError
10
+ from .models import APIError, TermsOfUseRequiredError, EmailVerificationRequiredError
11
11
 
12
12
 
13
13
  def load_creds_from_file(path: str) -> ClientCredentials:
@@ -72,8 +72,10 @@ def build_arg_parser() -> argparse.ArgumentParser:
72
72
  p.add_argument(
73
73
  "-s", "--shots",
74
74
  type=int,
75
- default=100,
76
- help="Number of shots"
75
+ default=None,
76
+ help=(
77
+ "Prefer configuration_data.shots long-term."
78
+ ),
77
79
  )
78
80
 
79
81
  # --- Auth ---
@@ -192,6 +194,9 @@ def main():
192
194
  except TermsOfUseRequiredError as e:
193
195
  print(str(e), file=sys.stderr)
194
196
  sys.exit(1)
197
+ except EmailVerificationRequiredError as e:
198
+ print(str(e), file=sys.stderr)
199
+ sys.exit(1)
195
200
  except requests.exceptions.HTTPError as e:
196
201
  print(f"API error: {e.response.status_code} - {e.response.text}", file=sys.stderr)
197
202
  sys.exit(1)
@@ -68,8 +68,8 @@ class BackendCapabilities:
68
68
  the DeviceFeatures enum. These drive precompilation,
69
69
  validation, and SDK-side decisions before jobs reach QPUs.
70
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"``.
71
+ accepts (e.g. ``"qasm"``). Defaults to ``["qasm"]``.
72
+ OpenQASM 2 and 3 both map to ``"qasm"``.
73
73
  """
74
74
 
75
75
  short_code: str
@@ -12,6 +12,7 @@ from .utils import strip_gate_definitions
12
12
  from .models import (
13
13
  APIError,
14
14
  TermsOfUseRequiredError,
15
+ EmailVerificationRequiredError,
15
16
  error_from_response,
16
17
  is_not_found_error,
17
18
  BackendClassRead,
@@ -54,7 +55,10 @@ class JobSubmissionConfig:
54
55
  backend_class_id: Backend class ID or short_code (e.g., ``"ionq:aria-1"``).
55
56
  name: Human-readable name for the job (visible in the portal).
56
57
  job_subcategory_id: Job subcategory ID or short_code (e.g., ``"phys:oth"``).
57
- shots: Number of measurement shots to execute.
58
+ shots: Optional measurement shot count (legacy). Prefer
59
+ ``configuration_data["shots"]``. Omit when the backend does not
60
+ use shots. When set, the SDK dual-writes into configuration_data
61
+ if missing.
58
62
  organization_id: Organization UUID. Optional if the user belongs to
59
63
  exactly one organization — the SDK resolves it automatically.
60
64
  configuration_data: Optional backend-specific configuration dictionary.
@@ -71,10 +75,10 @@ class JobSubmissionConfig:
71
75
  Defaults to 86 400 (1 day).
72
76
  submitted_with: Client/surface that submitted this preparation
73
77
  (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).
78
+ input_format: Circuit input language family (e.g. ``"qasm"``).
79
+ Defaults to ``"qasm"``. OpenQASM 2/3 both use ``"qasm"``.
80
+ Use the format advertised by the backend's
81
+ ``supported_formats`` for non-QASM payloads.
78
82
 
79
83
  Example:
80
84
  >>> config = JobSubmissionConfig(
@@ -89,7 +93,7 @@ class JobSubmissionConfig:
89
93
  backend_class_id: str
90
94
  name: str
91
95
  job_subcategory_id: str
92
- shots: int
96
+ shots: Optional[int] = None
93
97
  organization_id: Optional[str] = None
94
98
  configuration_data: Optional[Dict[str, Any]] = None
95
99
 
@@ -105,7 +109,7 @@ class JobSubmissionConfig:
105
109
  # Attribution for analytics / provenance
106
110
  submitted_with: Optional[str] = "sdk"
107
111
 
108
- # Circuit language family (qasm | qblox_sequence). None → API default "qasm".
112
+ # Circuit language family (e.g. "qasm"). None → API default "qasm".
109
113
  input_format: Optional[str] = None
110
114
 
111
115
  # INTERNAL: Fixed preparation timeout and polling
@@ -178,7 +182,7 @@ class BaseClient:
178
182
  error_code=payload.get("error_code"),
179
183
  )
180
184
  raise error_from_response(err)
181
- except (TermsOfUseRequiredError, APIError):
185
+ except (TermsOfUseRequiredError, EmailVerificationRequiredError, APIError):
182
186
  raise
183
187
  except ValueError:
184
188
  pass
@@ -473,6 +477,25 @@ class SchedulerClient(BaseClient):
473
477
  "Content may not be JSON or is malformed."
474
478
  ) from exc
475
479
 
480
+ @staticmethod
481
+ def _normalize_shots_and_configuration(
482
+ shots: Optional[int],
483
+ configuration_data: Optional[Dict[str, Any]],
484
+ ) -> tuple[Optional[int], Dict[str, Any]]:
485
+ """Dual-write shots into configuration_data when first-class shots is set."""
486
+ config: Dict[str, Any] = dict(configuration_data or {})
487
+ config_shots = config.get("shots")
488
+ if shots is not None and config_shots is not None and shots != config_shots:
489
+ raise ValueError(
490
+ f"shots mismatch: first-class value ({shots}) differs from "
491
+ f"configuration_data.shots ({config_shots})"
492
+ )
493
+ if shots is not None and config_shots is None:
494
+ config["shots"] = shots
495
+ if shots is None and isinstance(config_shots, int):
496
+ shots = config_shots
497
+ return shots, config
498
+
476
499
  def prepare_job(self, preparation: JobPreparationCreate) -> JobPreparationResponse:
477
500
  """Submit a job preparation request.
478
501
 
@@ -486,11 +509,20 @@ class SchedulerClient(BaseClient):
486
509
  Returns:
487
510
  A ``JobPreparationResponse`` containing the preparation ``id``.
488
511
  """
512
+ shots, configuration_data = self._normalize_shots_and_configuration(
513
+ preparation.shots,
514
+ preparation.configuration_data,
515
+ )
489
516
  payload = {
490
517
  key: value
491
518
  for key, value in asdict(preparation).items()
492
519
  if value is not None
493
520
  }
521
+ payload["configuration_data"] = configuration_data
522
+ if shots is not None:
523
+ payload["shots"] = shots
524
+ elif "shots" in payload:
525
+ del payload["shots"]
494
526
  data = self._request("POST", "/v1/jobs/prepare", json=payload)
495
527
  return JobPreparationResponse.from_dict(data)
496
528
 
@@ -548,7 +580,7 @@ class SchedulerClient(BaseClient):
548
580
  input_data_url=data["input_data_url"],
549
581
  job_category_id=data["job_category_id"],
550
582
  job_subcategory_id=data["job_subcategory_id"],
551
- shots=data["shots"],
583
+ shots=data.get("shots"),
552
584
  message=data.get("message"),
553
585
  quote=quote,
554
586
  configuration_data=data.get("configuration_data", {}),
@@ -559,10 +591,12 @@ class SchedulerClient(BaseClient):
559
591
  """Create and submit a job from a completed preparation.
560
592
 
561
593
  This deducts credits and places the job in the execution queue.
594
+ Organization is taken from the preparation; ``organization_id`` on
595
+ ``JobCreate`` is optional.
562
596
 
563
597
  Args:
564
- job: A ``JobCreate`` with the organization ID, preparation ID,
565
- chosen execution plan, and queue priority.
598
+ job: A ``JobCreate`` with the preparation ID, chosen execution
599
+ plan, and queue priority.
566
600
 
567
601
  Returns:
568
602
  A ``JobRead`` with the new job's ID and initial status.
@@ -580,7 +614,8 @@ class SchedulerClient(BaseClient):
580
614
  )
581
615
  self._public_plan_notice_shown = True
582
616
 
583
- data = self._request("POST", "/v1/jobs", json=asdict(job))
617
+ payload = {k: v for k, v in asdict(job).items() if v is not None}
618
+ data = self._request("POST", "/v1/jobs", json=payload)
584
619
  return JobRead.from_dict(data)
585
620
 
586
621
  def get_job(self, job_id: str) -> JobRead:
@@ -700,14 +735,18 @@ class SchedulerClient(BaseClient):
700
735
  print("Step 1/5: Uploading job input...")
701
736
  upload_id = self.upload_job_input(file_content=file_content, file_path=file_path)
702
737
 
738
+ shots, configuration_data = self._normalize_shots_and_configuration(
739
+ config.shots,
740
+ config.configuration_data or {},
741
+ )
703
742
  prep = JobPreparationCreate(
704
743
  organization_id=organization_id,
705
744
  backend_class_id=config.backend_class_id,
706
745
  name=config.name,
707
746
  upload_endpoint_id=upload_id,
708
747
  job_subcategory_id=config.job_subcategory_id,
709
- shots=config.shots,
710
- configuration_data=config.configuration_data or {},
748
+ shots=shots,
749
+ configuration_data=configuration_data,
711
750
  submitted_with=config.submitted_with,
712
751
  input_format=config.input_format,
713
752
  )
@@ -744,7 +783,6 @@ class SchedulerClient(BaseClient):
744
783
  print(f"Step 3/5: Quote approved automatically ({reason})")
745
784
 
746
785
  job_create = JobCreate(
747
- organization_id=organization_id,
748
786
  job_preparation_id=prep_resp.id,
749
787
  execution_plan_id=exec_plan_id,
750
788
  queue_priority_id=queue_prio_id,
@@ -56,6 +56,7 @@ class ApiErrorType:
56
56
  INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS"
57
57
  ORG_MEMBERSHIP_REQUIRED = "ORG_MEMBERSHIP_REQUIRED"
58
58
  TERMS_OF_USE_REQUIRED = "TERMS_OF_USE_REQUIRED"
59
+ EMAIL_VERIFICATION_REQUIRED = "EMAIL_VERIFICATION_REQUIRED"
59
60
  NOT_FOUND = "NOT_FOUND"
60
61
  CONFLICT = "CONFLICT"
61
62
  VALIDATION_ERROR = "VALIDATION_ERROR"
@@ -110,6 +111,10 @@ TERMS_OF_USE_REQUIRED_MESSAGE = (
110
111
  "Please visit https://www.openquantum.com to accept the Terms of Use before submitting jobs."
111
112
  )
112
113
 
114
+ EMAIL_VERIFICATION_REQUIRED_MESSAGE = (
115
+ "Please verify your email address at https://www.openquantum.com before submitting jobs."
116
+ )
117
+
113
118
 
114
119
  class TermsOfUseRequiredError(APIError):
115
120
  """Raised when job prepare/create is blocked until Terms of Use are accepted.
@@ -131,6 +136,25 @@ class TermsOfUseRequiredError(APIError):
131
136
  Exception.__init__(self, TERMS_OF_USE_REQUIRED_MESSAGE)
132
137
 
133
138
 
139
+ class EmailVerificationRequiredError(APIError):
140
+ """Raised when job prepare/create is blocked until the user's email is verified.
141
+
142
+ Users must visit https://www.openquantum.com, sign in, and complete email
143
+ verification before submitting jobs. If an SDK key was created before
144
+ verification, create a new key after verifying.
145
+ """
146
+
147
+ def __init__(self, error_response: Optional[ErrorResponse] = None):
148
+ if error_response is None:
149
+ error_response = ErrorResponse(
150
+ status_code=403,
151
+ message=EMAIL_VERIFICATION_REQUIRED_MESSAGE,
152
+ type=ApiErrorType.EMAIL_VERIFICATION_REQUIRED,
153
+ )
154
+ self.error_response = error_response
155
+ Exception.__init__(self, EMAIL_VERIFICATION_REQUIRED_MESSAGE)
156
+
157
+
134
158
  def error_from_response(error_response: ErrorResponse) -> APIError:
135
159
  """Map an API ``ErrorResponse`` to the most specific SDK exception.
136
160
 
@@ -138,6 +162,8 @@ def error_from_response(error_response: ErrorResponse) -> APIError:
138
162
  """
139
163
  if error_response.type == ApiErrorType.TERMS_OF_USE_REQUIRED:
140
164
  return TermsOfUseRequiredError(error_response)
165
+ if error_response.type == ApiErrorType.EMAIL_VERIFICATION_REQUIRED:
166
+ return EmailVerificationRequiredError(error_response)
141
167
  return APIError(error_response)
142
168
 
143
169
 
@@ -323,16 +349,18 @@ class JobPreparationCreate:
323
349
  upload_endpoint_id: ID returned by the upload endpoint for the
324
350
  input data file.
325
351
  job_subcategory_id: Job subcategory UUID or short code.
326
- shots: Number of measurement shots to execute.
352
+ shots: Optional measurement shot count (legacy first-class field).
353
+ Prefer ``configuration_data["shots"]``. Omit for backends that
354
+ do not use shots. When set, the SDK dual-writes into
355
+ configuration_data if missing.
327
356
  configuration_data: Optional backend-specific configuration.
328
357
  submitted_with: Client/surface that submitted this preparation
329
358
  (e.g. ``"sdk"``, ``"qiskit"``, ``"pennylane"``, ``"cli"``).
330
359
  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``.
360
+ input_format: Circuit input language family (e.g. ``"qasm"``).
361
+ Defaults to ``"qasm"``. OpenQASM 2 and 3 both use ``"qasm"``;
362
+ version is detected from the file. Must be listed in the
363
+ backend's ``constraint_data.supported_formats``.
336
364
  """
337
365
 
338
366
  organization_id: str
@@ -340,7 +368,7 @@ class JobPreparationCreate:
340
368
  name: str
341
369
  upload_endpoint_id: str
342
370
  job_subcategory_id: str
343
- shots: int
371
+ shots: Optional[int] = None
344
372
  configuration_data: Dict[str, Any] = field(default_factory=dict)
345
373
  submitted_with: Optional[str] = None
346
374
  input_format: Optional[str] = None
@@ -433,12 +461,12 @@ class JobPreparationResultResponse:
433
461
  input_data_url: URL where the input data is stored.
434
462
  job_category_id: Resolved job category UUID.
435
463
  job_subcategory_id: Resolved job subcategory UUID.
436
- shots: Number of measurement shots.
464
+ shots: Resolved shot count when applicable; may be ``None`` for jobs
465
+ without shots.
437
466
  quote: List of available execution plans with pricing details.
438
467
  message: Optional status or error message.
439
468
  configuration_data: Backend-specific configuration echoed back.
440
- input_format: Circuit input language family (``"qasm"`` or
441
- ``"qblox_sequence"``).
469
+ input_format: Circuit input language family (e.g. ``"qasm"``).
442
470
  extra: Additional fields returned by the API not mapped to attributes.
443
471
  """
444
472
 
@@ -449,7 +477,7 @@ class JobPreparationResultResponse:
449
477
  input_data_url: str
450
478
  job_category_id: str
451
479
  job_subcategory_id: str
452
- shots: int
480
+ shots: Optional[int]
453
481
  quote: List[QuotePlan]
454
482
  message: Optional[str] = None
455
483
  configuration_data: Dict[str, Any] = field(default_factory=dict)
@@ -464,16 +492,18 @@ class JobCreate:
464
492
  Sent to ``POST /v1/jobs`` to finalize and enqueue the job.
465
493
 
466
494
  Attributes:
467
- organization_id: UUID of the organization submitting the job.
468
495
  job_preparation_id: ID of the completed preparation.
469
496
  execution_plan_id: UUID of the chosen execution plan from the quote.
470
497
  queue_priority_id: UUID of the chosen queue priority from the quote.
498
+ organization_id: Optional organization UUID. If omitted, the server
499
+ uses the organization from the job preparation. If provided, it
500
+ must match the preparation organization.
471
501
  """
472
502
 
473
- organization_id: str
474
503
  job_preparation_id: str
475
504
  execution_plan_id: str
476
505
  queue_priority_id: str
506
+ organization_id: Optional[str] = None
477
507
 
478
508
 
479
509
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openquantum-sdk
3
- Version: 0.4.0
3
+ Version: 0.4.2
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,10 +8,17 @@ 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: 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"
11
18
  Provides-Extra: qiskit
12
- Requires-Dist: openquantum-sdk-qiskit>=0.3.0; extra == "qiskit"
19
+ Requires-Dist: openquantum-sdk-qiskit>=0.3.1; extra == "qiskit"
13
20
  Provides-Extra: pennylane
14
- Requires-Dist: openquantum-sdk-pennylane>=0.2.0; extra == "pennylane"
21
+ Requires-Dist: openquantum-sdk-pennylane>=0.2.1; extra == "pennylane"
15
22
 
16
23
  # Open Quantum Python SDK
17
24
 
@@ -35,7 +42,7 @@ pip install -U openquantum-sdk
35
42
  pip install -e .
36
43
  ```
37
44
 
38
- Python 3.9+ required.
45
+ Python 3.8+ required.
39
46
 
40
47
  ## Authentication
41
48
 
@@ -220,9 +227,8 @@ try:
220
227
  cheapest_plan = min(result.quote, key=lambda p: p.price)
221
228
  cheapest_prio = min(cheapest_plan.queue_priorities, key=lambda q: q.price_increase)
222
229
 
223
- # 7. Create job
230
+ # 7. Create job (organization comes from the preparation)
224
231
  job = JobCreate(
225
- organization_id=org.id,
226
232
  job_preparation_id=prep_resp.id,
227
233
  execution_plan_id=cheapest_plan.execution_plan_id,
228
234
  queue_priority_id=cheapest_prio.queue_priority_id,
@@ -320,7 +326,7 @@ finally:
320
326
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
321
327
  | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
322
328
  | `JobCategoryRead` | `id`, `name`, `short_code` |
323
- | `JobCreate` | `organization_id`, `job_preparation_id`, `execution_plan_id`, `queue_priority_id` |
329
+ | `JobCreate` | `job_preparation_id`, `execution_plan_id`, `queue_priority_id`, optional `organization_id` |
324
330
  | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
325
331
  | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
326
332
  | `JobPreparationResponse` | `id` |
@@ -0,0 +1,15 @@
1
+ requests>=2.25.0
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ pytest-cov
6
+ pytest-mock
7
+ requests-mock>=1.10.0
8
+ flake8
9
+ black
10
+
11
+ [pennylane]
12
+ openquantum-sdk-pennylane>=0.2.1
13
+
14
+ [qiskit]
15
+ openquantum-sdk-qiskit>=0.3.1
@@ -4,13 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openquantum-sdk"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Python SDK for Open Quantum"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
- # Remove 'dataclasses' on Python 3.11+; it's stdlib and the backport causes issues
12
11
  dependencies = [
13
- "requests>=2.25.0"
12
+ "requests>=2.25.0",
14
13
  ]
15
14
  classifiers = [
16
15
  "Programming Language :: Python :: 3",
@@ -19,8 +18,16 @@ classifiers = [
19
18
  ]
20
19
 
21
20
  [project.optional-dependencies]
22
- qiskit = ["openquantum-sdk-qiskit>=0.3.0"]
23
- pennylane = ["openquantum-sdk-pennylane>=0.2.0"]
21
+ dev = [
22
+ "pytest>=7.0",
23
+ "pytest-cov",
24
+ "pytest-mock",
25
+ "requests-mock>=1.10.0",
26
+ "flake8",
27
+ "black",
28
+ ]
29
+ qiskit = ["openquantum-sdk-qiskit>=0.3.1"]
30
+ pennylane = ["openquantum-sdk-pennylane>=0.2.1"]
24
31
 
25
32
  [project.scripts]
26
33
  openquantum-sdk = "openquantum_sdk.__main__:main"
@@ -1,2 +1 @@
1
1
  requests>=2.25.0
2
- dataclasses
@@ -20,6 +20,8 @@ from openquantum_sdk.models import (
20
20
  OrganizationRead,
21
21
  TERMS_OF_USE_REQUIRED_MESSAGE,
22
22
  TermsOfUseRequiredError,
23
+ EMAIL_VERIFICATION_REQUIRED_MESSAGE,
24
+ EmailVerificationRequiredError,
23
25
  )
24
26
 
25
27
 
@@ -499,7 +501,6 @@ def test_create_job_terms_of_use_required(mock_scheduler):
499
501
  },
500
502
  )
501
503
  job = JobCreate(
502
- organization_id="org-123",
503
504
  job_preparation_id="prep-1",
504
505
  execution_plan_id="plan-1",
505
506
  queue_priority_id="prio-1",
@@ -508,6 +509,55 @@ def test_create_job_terms_of_use_required(mock_scheduler):
508
509
  mock_scheduler.create_job(job)
509
510
 
510
511
 
512
+ def test_prepare_job_email_verification_required(mock_scheduler):
513
+ mock_scheduler.session.mocker.post(
514
+ "https://scheduler.openquantum.com/v1/jobs/prepare",
515
+ status_code=403,
516
+ json={
517
+ "status_code": 403,
518
+ "message": [EMAIL_VERIFICATION_REQUIRED_MESSAGE],
519
+ "type": "EMAIL_VERIFICATION_REQUIRED",
520
+ "error_code": "req-email",
521
+ },
522
+ )
523
+ prep = JobPreparationCreate(
524
+ organization_id="org-123",
525
+ backend_class_id="backend-1",
526
+ name="Test",
527
+ upload_endpoint_id="upload-1",
528
+ job_subcategory_id="sub-1",
529
+ shots=10,
530
+ configuration_data={},
531
+ )
532
+ with pytest.raises(
533
+ EmailVerificationRequiredError, match="verify your email"
534
+ ) as exc_info:
535
+ mock_scheduler.prepare_job(prep)
536
+ assert str(exc_info.value) == EMAIL_VERIFICATION_REQUIRED_MESSAGE
537
+ assert exc_info.value.error_response.status_code == 403
538
+ assert exc_info.value.error_response.type == "EMAIL_VERIFICATION_REQUIRED"
539
+
540
+
541
+ def test_create_job_email_verification_required(mock_scheduler):
542
+ mock_scheduler.session.mocker.post(
543
+ "https://scheduler.openquantum.com/v1/jobs",
544
+ status_code=403,
545
+ json={
546
+ "status_code": 403,
547
+ "message": EMAIL_VERIFICATION_REQUIRED_MESSAGE,
548
+ "type": "EMAIL_VERIFICATION_REQUIRED",
549
+ "error_code": "req-email",
550
+ },
551
+ )
552
+ job = JobCreate(
553
+ job_preparation_id="prep-1",
554
+ execution_plan_id="plan-1",
555
+ queue_priority_id="prio-1",
556
+ )
557
+ with pytest.raises(EmailVerificationRequiredError, match="email"):
558
+ mock_scheduler.create_job(job)
559
+
560
+
511
561
  def test_prepare_job_org_membership_required(mock_scheduler):
512
562
  from openquantum_sdk.models import APIError, ApiErrorType
513
563
 
@@ -533,6 +583,7 @@ def test_prepare_job_org_membership_required(mock_scheduler):
533
583
  with pytest.raises(APIError) as exc_info:
534
584
  mock_scheduler.prepare_job(prep)
535
585
  assert not isinstance(exc_info.value, TermsOfUseRequiredError)
586
+ assert not isinstance(exc_info.value, EmailVerificationRequiredError)
536
587
  assert (
537
588
  exc_info.value.error_response.type
538
589
  == ApiErrorType.ORG_MEMBERSHIP_REQUIRED
@@ -1,7 +0,0 @@
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