openquantum-sdk 0.4.1__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 (27) hide show
  1. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/PKG-INFO +3 -4
  2. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/README.md +2 -3
  3. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/__init__.py +5 -1
  4. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/__main__.py +4 -1
  5. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/clients.py +8 -5
  6. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/models.py +30 -2
  7. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/PKG-INFO +3 -4
  8. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/pyproject.toml +1 -1
  9. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/tests/test_clients.py +52 -1
  10. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/MANIFEST.in +0 -0
  11. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/auth.py +0 -0
  12. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/backends.py +0 -0
  13. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/enums.py +0 -0
  14. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/qiskit/__init__.py +0 -0
  15. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk/utils.py +0 -0
  16. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/SOURCES.txt +0 -0
  17. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/dependency_links.txt +0 -0
  18. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/entry_points.txt +0 -0
  19. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/requires.txt +0 -0
  20. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/openquantum_sdk.egg-info/top_level.txt +0 -0
  21. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/requirements-dev.txt +0 -0
  22. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/requirements.txt +0 -0
  23. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/setup.cfg +0 -0
  24. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/tests/__init__.py +0 -0
  25. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/tests/test_backends.py +0 -0
  26. {openquantum_sdk-0.4.1 → openquantum_sdk-0.4.2}/tests/test_clients_plan_priority.py +0 -0
  27. {openquantum_sdk-0.4.1 → 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.1
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
@@ -227,9 +227,8 @@ try:
227
227
  cheapest_plan = min(result.quote, key=lambda p: p.price)
228
228
  cheapest_prio = min(cheapest_plan.queue_priorities, key=lambda q: q.price_increase)
229
229
 
230
- # 7. Create job
230
+ # 7. Create job (organization comes from the preparation)
231
231
  job = JobCreate(
232
- organization_id=org.id,
233
232
  job_preparation_id=prep_resp.id,
234
233
  execution_plan_id=cheapest_plan.execution_plan_id,
235
234
  queue_priority_id=cheapest_prio.queue_priority_id,
@@ -327,7 +326,7 @@ finally:
327
326
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
328
327
  | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
329
328
  | `JobCategoryRead` | `id`, `name`, `short_code` |
330
- | `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` |
331
330
  | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
332
331
  | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
333
332
  | `JobPreparationResponse` | `id` |
@@ -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.1"
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:
@@ -194,6 +194,9 @@ def main():
194
194
  except TermsOfUseRequiredError as e:
195
195
  print(str(e), file=sys.stderr)
196
196
  sys.exit(1)
197
+ except EmailVerificationRequiredError as e:
198
+ print(str(e), file=sys.stderr)
199
+ sys.exit(1)
197
200
  except requests.exceptions.HTTPError as e:
198
201
  print(f"API error: {e.response.status_code} - {e.response.text}", file=sys.stderr)
199
202
  sys.exit(1)
@@ -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,
@@ -181,7 +182,7 @@ class BaseClient:
181
182
  error_code=payload.get("error_code"),
182
183
  )
183
184
  raise error_from_response(err)
184
- except (TermsOfUseRequiredError, APIError):
185
+ except (TermsOfUseRequiredError, EmailVerificationRequiredError, APIError):
185
186
  raise
186
187
  except ValueError:
187
188
  pass
@@ -590,10 +591,12 @@ class SchedulerClient(BaseClient):
590
591
  """Create and submit a job from a completed preparation.
591
592
 
592
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.
593
596
 
594
597
  Args:
595
- job: A ``JobCreate`` with the organization ID, preparation ID,
596
- chosen execution plan, and queue priority.
598
+ job: A ``JobCreate`` with the preparation ID, chosen execution
599
+ plan, and queue priority.
597
600
 
598
601
  Returns:
599
602
  A ``JobRead`` with the new job's ID and initial status.
@@ -611,7 +614,8 @@ class SchedulerClient(BaseClient):
611
614
  )
612
615
  self._public_plan_notice_shown = True
613
616
 
614
- 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)
615
619
  return JobRead.from_dict(data)
616
620
 
617
621
  def get_job(self, job_id: str) -> JobRead:
@@ -779,7 +783,6 @@ class SchedulerClient(BaseClient):
779
783
  print(f"Step 3/5: Quote approved automatically ({reason})")
780
784
 
781
785
  job_create = JobCreate(
782
- organization_id=organization_id,
783
786
  job_preparation_id=prep_resp.id,
784
787
  execution_plan_id=exec_plan_id,
785
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
 
@@ -466,16 +492,18 @@ class JobCreate:
466
492
  Sent to ``POST /v1/jobs`` to finalize and enqueue the job.
467
493
 
468
494
  Attributes:
469
- organization_id: UUID of the organization submitting the job.
470
495
  job_preparation_id: ID of the completed preparation.
471
496
  execution_plan_id: UUID of the chosen execution plan from the quote.
472
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.
473
501
  """
474
502
 
475
- organization_id: str
476
503
  job_preparation_id: str
477
504
  execution_plan_id: str
478
505
  queue_priority_id: str
506
+ organization_id: Optional[str] = None
479
507
 
480
508
 
481
509
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openquantum-sdk
3
- Version: 0.4.1
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
@@ -227,9 +227,8 @@ try:
227
227
  cheapest_plan = min(result.quote, key=lambda p: p.price)
228
228
  cheapest_prio = min(cheapest_plan.queue_priorities, key=lambda q: q.price_increase)
229
229
 
230
- # 7. Create job
230
+ # 7. Create job (organization comes from the preparation)
231
231
  job = JobCreate(
232
- organization_id=org.id,
233
232
  job_preparation_id=prep_resp.id,
234
233
  execution_plan_id=cheapest_plan.execution_plan_id,
235
234
  queue_priority_id=cheapest_prio.queue_priority_id,
@@ -327,7 +326,7 @@ finally:
327
326
  | `BackendClassRead` | `id`, `name`, `description`, `type`, `provider_id`, `short_code`, `queue_depth`, `accepting_jobs`, `status` |
328
327
  | `ErrorResponse` | `status_code`, `message: List[str]`, `type`, `error_code` (request id) |
329
328
  | `JobCategoryRead` | `id`, `name`, `short_code` |
330
- | `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` |
331
330
  | `JobList` | `id`, `backend_class_id`, `name`, `status`, `backend_name`, `output_data_url`, `calibration_data_url`, `credits_used`, `submitted_at` |
332
331
  | `JobPreparationCreate` | `organization_id`, `backend_class_id`, `name`, `upload_endpoint_id`, `job_subcategory_id`, `shots`, `configuration_data`, `input_format`, `submitted_with` |
333
332
  | `JobPreparationResponse` | `id` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openquantum-sdk"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Python SDK for Open Quantum"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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