oqtant .oqtant_client
1# Copyright 2023 Infleqtion 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import json 16import os 17import sys 18import time 19import warnings 20from importlib.metadata import version 21 22import jwt 23import requests 24import semver 25from bert_schemas import job as job_schema 26from pydantic import ValidationError 27 28from oqtant.schemas.job import OqtantJob 29from oqtant.settings import Settings 30from oqtant.util import exceptions as api_exceptions 31 32settings = Settings() 33 34 35class OqtantClient: 36 """Python class for interacting with Oqtant 37 This class contains tools for: 38 - Accessing all of the functionality of the Oqtant Web App (https://oqtant.infleqtion.com) 39 - BARRIER (Barrier Manipulator) jobs 40 - BEC (Ultracold Matter) jobs 41 - Building parameterized (i.e. optimization) experiments using OqtantJobs 42 - Submitting and retrieving OqtantJob results 43 How Oqtant works: 44 1.) Construct a single or list of OqtantJobs using 'generate_oqtant_job()' 45 2.) Run the single or list of OqtantJobs on the Oqtant hardware using 'run_jobs()' 46 - There is a limit of 30 OqtantJobs per use of 'run_jobs()' 47 3.) As OqtantJobs are running, the results are automatically stored in 'active_jobs' 48 - The OqtantJobs stored in 'active_jobs' are available until the python session ends 49 4.) If you choose to not track the status of OqtantJobs with 'run_jobs()' you can see the status 50 of your session's active OqtantJobs with 'see_active_jobs()' 51 5.) To operate on jobs submitted in a previous session you can load them into your 'active_jobs' 52 by using either 'load_job_from_id()' or 'load_job_from_file()' 53 6.) To analyze OqtantJob objects and use Oqtant's job analysis library reference the OqtantJob 54 class documentation located in 'oqtant/job.py' 55 Need help? Found a bug? Contact albert@infleqtion.com for support. Thank you! 56 """ 57 58 def __init__(self, *, settings, token, debug: bool = False): 59 self.base_url: str = settings.base_url 60 self.active_jobs: dict[str, OqtantJob] = {} 61 self.token: str = token 62 self.max_ind_var: int = settings.max_ind_var 63 self.max_job_batch_size: int = settings.max_job_batch_size 64 self.debug: bool = debug 65 self.version = version("oqtant") 66 67 if not self.debug: 68 sys.tracebacklimit = 0 69 70 def __get_headers(self) -> dict: 71 """Generate headers for use in calls to the REST API with requests 72 Returns: 73 dict: a dict of header information 74 """ 75 return { 76 "Authorization": f"Bearer {self.token}", 77 "X-Client-Version": version("oqtant"), 78 } 79 80 def get_job(self, job_id: str, run: int = 1) -> OqtantJob: 81 """Gets an OqtantJob from the Oqtant REST API. This will always be a targeted query 82 for a specific run. If the run is omitted then this will always return the first 83 run of the job. Will return results for any job regardless of it's status. 84 Args: 85 job_id (str): this is the external_id of the job to fetch 86 run (int): the run to target, this defaults to the first run if omitted 87 Returns: 88 OqtantJob: an OqtantJob instance with the values of the job queried 89 """ 90 request_url = f"{self.base_url}/{job_id}" 91 params = {"run": run} 92 response = requests.get( 93 url=request_url, 94 params=params, 95 headers=self.__get_headers(), 96 timeout=(5, 30), 97 ) 98 if response.status_code in [401, 403]: 99 raise api_exceptions.AuthorizationError 100 response.raise_for_status() 101 job_data = response.json() 102 try: 103 job = OqtantJob(**job_data) 104 except (KeyError, ValidationError) as err: 105 raise api_exceptions.ValidationError(f"Failed to get job {job_id}: {err}") 106 return job 107 108 def get_job_inputs_without_output( 109 self, job_id: str, run: int | None = None, include_notes: bool = False 110 ) -> dict: 111 """Gets an OqtantJob from the Oqtant REST API. This can return all runs within a job 112 or a single run based on whether a run value is provided. The OqtantJobs returned 113 will be converted to dictionaries and will not have any output data, even if 114 they are complete. This is useful for taking an existing job and creating a new one 115 based on it's input data. 116 Args: 117 job_id (str): this is the external_id of the job to fetch 118 run (Union[int, None]): optional argument if caller wishes to only has a single run returned 119 include_notes (bool): optional argument if caller wishes to include any notes associated 120 with OqtantJob inputs. Defaults to False is not provided 121 Returns: 122 dict: a dict representation of an OqtantJob instance 123 """ 124 request_url = f"{self.base_url}/{job_id}" 125 params = {"exclude_input_output": True} 126 if run: 127 params["run"] = run 128 response = requests.get( 129 url=request_url, 130 params=params, 131 headers=self.__get_headers(), 132 timeout=(5, 30), 133 ) 134 if response.status_code in [401, 403]: 135 raise api_exceptions.AuthorizationError 136 response.raise_for_status() 137 job_data = response.json() 138 try: 139 job = OqtantJob(**job_data) 140 except (KeyError, ValidationError) as err: 141 raise api_exceptions.ValidationError(f"Failed to get job {job_id}: {err}") 142 if not include_notes: 143 job.inputs[0].notes = "" 144 job = job.dict() 145 job.pop("input_count") 146 return job 147 148 def generate_oqtant_job(self, *, job: dict) -> OqtantJob: 149 """Generates an instance of OqtantJob from the provided dictionary that contains the 150 job details and input. Will validate the values and raise an informative error if 151 any violations are found. 152 Args: 153 job (dict): dictionary containing job details and input 154 Returns: 155 OqtantJob: an OqtantJob instance containing the details and input from the provided 156 dictionary 157 """ 158 try: 159 oqtant_job = OqtantJob(**job) 160 except ValidationError as err: 161 raise api_exceptions.ValidationError(f"Failed to generate OqtantJob: {err}") 162 return oqtant_job 163 164 def submit_job(self, *, job: OqtantJob) -> dict: 165 """Submits a single OqtantJob to the Oqtant REST API. Upon successful submission this 166 function will return a dictionary containing the external_id of the job and it's 167 position in the queue. 168 Args: 169 job (OqtantJob): the OqtantJob instance to submit for processing 170 Returns: 171 dict: dictionary containing the external_id of the job and it's queue position 172 """ 173 if not isinstance(job, OqtantJob): 174 try: 175 job = OqtantJob(**job) 176 except (TypeError, AttributeError, ValidationError) as err: 177 raise api_exceptions.ValidationError(f"Job is invalid: {err}") 178 data = { 179 "name": job.name, 180 "job_type": job.job_type, 181 "input_count": len(job.inputs), 182 "inputs": [input.dict() for input in job.inputs], 183 } 184 response = requests.post( 185 url=self.base_url, 186 json=data, 187 headers=self.__get_headers(), 188 timeout=(5, 30), 189 ) 190 if response.status_code in [401, 403]: 191 raise api_exceptions.AuthorizationError 192 response.raise_for_status() 193 response_data = response.json() 194 return response_data 195 196 def run_jobs( 197 self, 198 job_list: list[OqtantJob], 199 track_status: bool = False, 200 write: bool = False, 201 filename: str | list[str] = "", 202 ) -> list[str]: 203 """Submits a list of OqtantJobs to the Oqtant REST API. This function provides some 204 optional functionality to alter how it behaves. Providing it with an argument of 205 track_status=True will make it wait and poll the Oqtant REST API until all jobs 206 in the list have completed. The track_status functionality outputs each jobs 207 current status as it is polling and opens up the ability to use the other optional 208 arguments write and filename. The write and filename arguments enable the ability 209 to have the results of each completed job written to a file. The value of filename 210 is optional and if not provided will cause the files to be created using the 211 external_id of each job. If running more than one job and using the filename 212 argument it is required that the number of jobs in job_list match the number of 213 values in filename. 214 Args: 215 job_list (list[OqtantJob]): the list of OqtantJob instances to submit for processing 216 track_status (bool): optional argument to tell this function to either return 217 immediately or wait and poll until all jobs have completed 218 write (bool): optional argument to tell this function to write the results of each 219 job to file when complete 220 filename (Union[str, list[str]]): optional argument to be used in conjunction with the 221 write argument. allows the caller to customize the name(s) of the files being created 222 Returns: 223 list[str]: list of the external_id(s) returned for each submitted job in job_list 224 """ 225 if len(job_list) > self.max_job_batch_size: 226 raise AttributeError( 227 f"Maximum number of jobs submitted per run is {self.max_job_batch_size}." 228 ) 229 pending_jobs = [] 230 submitted_jobs = [] 231 for job in job_list: 232 response = self.submit_job(job=job) 233 external_id = response["job_id"] 234 queue_position = response["queue_position"] 235 est_time = response["est_time"] 236 237 job.external_id = external_id 238 self.active_jobs[external_id] = job 239 240 pending_jobs.append(external_id) 241 submitted_jobs.append(external_id) 242 print( 243 f"submitted {job.name} ID: {job.external_id} queue_position: {queue_position} \ 244 est. time: {est_time} minutes" 245 ) 246 print("Jobs submitted: \n") 247 if track_status: 248 self.track_jobs(pending_jobs=pending_jobs, filename=filename, write=write) 249 return submitted_jobs 250 251 def search_jobs( 252 self, 253 *, 254 job_type: job_schema.JobType | None = None, 255 name: job_schema.JobName | None = None, 256 submit_start: str | None = None, 257 submit_end: str | None = None, 258 notes: str | None = None, 259 ) -> list[dict]: 260 """Submits a query to the Oqtant REST API to search for jobs that match the provided criteria. 261 The search results will be limited to jobs that meet your Oqtant account access. 262 Args: 263 job_type (job_schema.JobType): the type of the jobs to search for 264 name (job_schema.JobName): the name of the job to search for 265 submit_start (str): the earliest submit date of the jobs to search for 266 submit_start (str): the latest submit date of the jobs to search for 267 notes (str): the notes of the jobs to search for 268 Returns: 269 list[dict]: a list of jobs matching the provided search criteria 270 """ 271 request_url = f"{self.base_url}/" 272 params = {} 273 for param in ["job_type", "name", "submit_start", "submit_end", "notes"]: 274 if locals()[param] is not None: 275 params[param] = locals()[param] 276 277 response = requests.get( 278 url=request_url, 279 params=params, 280 headers=self.__get_headers(), 281 timeout=(5, 30), 282 ) 283 if response.status_code in [401, 403]: 284 raise api_exceptions.AuthorizationError 285 response.raise_for_status() 286 job_data = response.json() 287 return job_data["items"] 288 289 def track_jobs( 290 self, 291 *, 292 pending_jobs: list[str], 293 filename: str | list = "", 294 write: bool = False, 295 ) -> None: 296 """Polls the Oqtant REST API with a list of job external_ids and waits until all of them have 297 completed. Will output each job's status while it is polling and will output a message when 298 all jobs have completed. This function provides some optional functionality to alter how it 299 behaves. Providing it with an argument of write will have it write the results of each 300 completed job to a file. There is an additional argument that can be used with write called 301 filename. The value of filename is optional and if not provided will cause the files to be 302 created using the external_id of each job. If tracking more than one job and using the 303 filename argument it is required that the number of jobs in job_list match the number of 304 values in filename. 305 Args: 306 pending_jobs (list[str]): list of job external_ids to track 307 write (bool): optional argument to tell this function to write the results of each job to 308 file when complete 309 filename (Union[str, list[str]]): optional argument to be used in conjunction with the write 310 argument. allows the caller to customize the name(s) of the files being created 311 """ 312 if write: 313 if not filename: 314 job_filenames = [f"{external_id}.txt" for external_id in pending_jobs] 315 elif isinstance(filename, str): 316 job_filenames = [filename] 317 else: 318 job_filenames = filename 319 if len(job_filenames) != len(pending_jobs): 320 raise AttributeError( 321 "Write filename list length does not match number of jobs." 322 ) 323 324 status = None 325 while pending_jobs: 326 for pending_job in pending_jobs: 327 job = self.get_job(job_id=pending_job) 328 self.active_jobs[pending_job] = job 329 if job.status != status: 330 print(f"job {pending_job} is in status {job.status}") 331 status = job.status 332 if job.status == job_schema.JobStatus.COMPLETE: 333 print(f"job complete: {pending_job}") 334 pending_jobs.remove(pending_job) 335 if write: 336 self._write_job_to_file( 337 self.active_jobs[pending_job], job_filenames.pop(0) 338 ) 339 if job.status in [ 340 job_schema.JobStatus.INCOMPLETE, 341 job_schema.JobStatus.FAILED, 342 ]: 343 pending_jobs.remove(pending_job) 344 if write: 345 job_filenames.pop(0) 346 time.sleep(2) 347 print("all jobs complete") 348 349 def load_job_from_id_list(self, job_id_list: list[str]) -> None: 350 """Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list 351 of job external_ids. The results of the jobs loaded by this function are limited to their 352 first run. 353 Args: 354 job_id_list (list[str]): list of job external_ids to load 355 """ 356 for job_id in job_id_list: 357 self.load_job_from_id(job_id) 358 359 def load_job_from_id(self, job_id: str, run: int = 1) -> None: 360 """Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a job 361 external_id. The results of the jobs loaded by this function can be targeted to a specific 362 run if there are multiple. 363 Args: 364 job_id (str): the external_id of the job to load 365 run (int): optional argument to target a specific job run 366 """ 367 try: 368 job = self.get_job(job_id=job_id, run=run) 369 self.active_jobs[job_id] = job 370 print(f"Loaded job: {job.name} {job_id}") 371 except Exception as err: 372 raise api_exceptions.ValidationError( 373 f"Failed to fetch job {job_id}: {err}. Please contact ColdQuanta if error persists" 374 ) 375 376 def load_job_from_file_list(self, file_list: list[str]) -> None: 377 """Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list 378 of filenames containing OqtantJob info. The results of the jobs loaded by this function are 379 limited to their first run. 380 Args: 381 file_list (list[str]): list of filenames containing OqtantJob information 382 """ 383 for f in file_list: 384 self.load_job_from_file(f) 385 386 def load_job_from_file(self, file: str) -> None: 387 """Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a file 388 containing OqtantJob info. The results of the jobs loaded by this function are limited to 389 their first run. 390 Args: 391 file_list (list[str]): list of filenames containing OqtantJob information 392 """ 393 try: 394 with open(file) as json_file: 395 data = json.load(json_file) 396 self.load_job_from_id(data["external_id"]) 397 except (FileNotFoundError, Exception) as err: 398 raise api_exceptions.JobReadError(f"Failed to load job from {file}: {err}") 399 400 def see_active_jobs(self, refresh: bool = True) -> None: 401 """Utility function to print out the current contents of the active_jobs list. The optional 402 argument of refresh tells the function whether it should refresh the data of pending or 403 running jobs stored in active_jobs before printing out the results. Refreshing also 404 updates the data in active_jobs so if jobs were submitted but not tracked this is a way 405 to check on their status. 406 Args: 407 refresh (bool): optional argument to refresh the data of jobs in active_jobs 408 """ 409 if refresh: 410 for external_id, job in self.active_jobs.items(): 411 if job.status in [ 412 job_schema.JobStatus.PENDING, 413 job_schema.JobStatus.RUNNING, 414 ]: 415 refreshed_job = self.get_job( 416 job_id=external_id, run=job.inputs[0].run 417 ) 418 self.active_jobs[external_id] = refreshed_job 419 print("ACTIVE JOBS") 420 print("NAME\t\tSTATUS\t\tTIME SUBMIT\t\tID") 421 print("_" * 50) 422 for job_id, job in self.active_jobs.items(): 423 print(f"{job.name}\t\t{job.status}\t\t{job.time_submit}\t\t{job_id}") 424 425 def _write_job_to_file(self, job: OqtantJob, filepath: str) -> None: 426 """Utility function to write an OqtantJob instance to a file. Requires the full filepath 427 including the name of the file the write. Files that already exist cannot be 428 overwritten. 429 Args: 430 job (OqtantJob): the OqtantJob instance to write to file 431 filepath (str): the full path to the file to write, including the name of the file 432 """ 433 if os.path.exists(filepath): 434 raise api_exceptions.JobWriteError( 435 "File already exists. Please choose a unique filename." 436 ) 437 438 try: 439 with open(filepath, "w") as f: 440 f.write(str(job.json())) 441 print(f"Wrote job {job.external_id} to file {filepath}") 442 except (FileNotFoundError, Exception) as err: 443 print(f"Failed to write job {job.external_id} to {filepath}: {err}") 444 445 def get_job_limits(self) -> dict: 446 """Utility method to get job limits from the Oqtant REST API 447 Returns: 448 dict: dictionary of job limits 449 """ 450 try: 451 token_data = jwt.decode( 452 self.token, key=None, options={"verify_signature": False} 453 ) 454 external_user_id = token_data["sub"] 455 except Exception: 456 raise api_exceptions.ValidationError( 457 "Unable to decode JWT token. Please contact ColdQuanta." 458 ) 459 460 url = f"{self.base_url.replace('jobs', 'users')}/{external_user_id}/job_limits" 461 response = requests.get( 462 url=url, 463 headers=self.__get_headers(), 464 timeout=(5, 30), 465 ) 466 if response.status_code in [401, 403]: 467 raise api_exceptions.AuthorizationError("Unauthorized") 468 response.raise_for_status() 469 job_limits = response.json() 470 return job_limits 471 472 473def version_check(client_version: str) -> None: 474 """Compares the given current Oqtant version with the version currently on pypi, 475 and raises a warning if it is older. 476 Args: 477 client_version (str): the client semver version number 478 """ 479 resp = requests.get("https://pypi.org/pypi/oqtant/json", timeout=5) 480 if resp.status_code == 200: 481 current_version = resp.json()["info"]["version"] 482 if semver.compare(client_version, current_version) < 0: 483 warnings.warn( 484 f"Please upgrade to Oqtant version {current_version}. You are currently using version {client_version}." 485 ) 486 487 488def get_oqtant_client(token: str) -> OqtantClient: 489 """A utility function to create a new OqtantClient instance. 490 Args: 491 token (str): the auth0 token required for interacting with the Oqtant REST API 492 Returns: 493 OqtantClient: authenticated instance of OqtantClient 494 """ 495 496 client = OqtantClient(settings=settings, token=token) 497 version_check(client.version) 498 return client
36class OqtantClient: 37 """Python class for interacting with Oqtant 38 This class contains tools for: 39 - Accessing all of the functionality of the Oqtant Web App (https://oqtant.infleqtion.com) 40 - BARRIER (Barrier Manipulator) jobs 41 - BEC (Ultracold Matter) jobs 42 - Building parameterized (i.e. optimization) experiments using OqtantJobs 43 - Submitting and retrieving OqtantJob results 44 How Oqtant works: 45 1.) Construct a single or list of OqtantJobs using 'generate_oqtant_job()' 46 2.) Run the single or list of OqtantJobs on the Oqtant hardware using 'run_jobs()' 47 - There is a limit of 30 OqtantJobs per use of 'run_jobs()' 48 3.) As OqtantJobs are running, the results are automatically stored in 'active_jobs' 49 - The OqtantJobs stored in 'active_jobs' are available until the python session ends 50 4.) If you choose to not track the status of OqtantJobs with 'run_jobs()' you can see the status 51 of your session's active OqtantJobs with 'see_active_jobs()' 52 5.) To operate on jobs submitted in a previous session you can load them into your 'active_jobs' 53 by using either 'load_job_from_id()' or 'load_job_from_file()' 54 6.) To analyze OqtantJob objects and use Oqtant's job analysis library reference the OqtantJob 55 class documentation located in 'oqtant/job.py' 56 Need help? Found a bug? Contact albert@infleqtion.com for support. Thank you! 57 """ 58 59 def __init__(self, *, settings, token, debug: bool = False): 60 self.base_url: str = settings.base_url 61 self.active_jobs: dict[str, OqtantJob] = {} 62 self.token: str = token 63 self.max_ind_var: int = settings.max_ind_var 64 self.max_job_batch_size: int = settings.max_job_batch_size 65 self.debug: bool = debug 66 self.version = version("oqtant") 67 68 if not self.debug: 69 sys.tracebacklimit = 0 70 71 def __get_headers(self) -> dict: 72 """Generate headers for use in calls to the REST API with requests 73 Returns: 74 dict: a dict of header information 75 """ 76 return { 77 "Authorization": f"Bearer {self.token}", 78 "X-Client-Version": version("oqtant"), 79 } 80 81 def get_job(self, job_id: str, run: int = 1) -> OqtantJob: 82 """Gets an OqtantJob from the Oqtant REST API. This will always be a targeted query 83 for a specific run. If the run is omitted then this will always return the first 84 run of the job. Will return results for any job regardless of it's status. 85 Args: 86 job_id (str): this is the external_id of the job to fetch 87 run (int): the run to target, this defaults to the first run if omitted 88 Returns: 89 OqtantJob: an OqtantJob instance with the values of the job queried 90 """ 91 request_url = f"{self.base_url}/{job_id}" 92 params = {"run": run} 93 response = requests.get( 94 url=request_url, 95 params=params, 96 headers=self.__get_headers(), 97 timeout=(5, 30), 98 ) 99 if response.status_code in [401, 403]: 100 raise api_exceptions.AuthorizationError 101 response.raise_for_status() 102 job_data = response.json() 103 try: 104 job = OqtantJob(**job_data) 105 except (KeyError, ValidationError) as err: 106 raise api_exceptions.ValidationError(f"Failed to get job {job_id}: {err}") 107 return job 108 109 def get_job_inputs_without_output( 110 self, job_id: str, run: int | None = None, include_notes: bool = False 111 ) -> dict: 112 """Gets an OqtantJob from the Oqtant REST API. This can return all runs within a job 113 or a single run based on whether a run value is provided. The OqtantJobs returned 114 will be converted to dictionaries and will not have any output data, even if 115 they are complete. This is useful for taking an existing job and creating a new one 116 based on it's input data. 117 Args: 118 job_id (str): this is the external_id of the job to fetch 119 run (Union[int, None]): optional argument if caller wishes to only has a single run returned 120 include_notes (bool): optional argument if caller wishes to include any notes associated 121 with OqtantJob inputs. Defaults to False is not provided 122 Returns: 123 dict: a dict representation of an OqtantJob instance 124 """ 125 request_url = f"{self.base_url}/{job_id}" 126 params = {"exclude_input_output": True} 127 if run: 128 params["run"] = run 129 response = requests.get( 130 url=request_url, 131 params=params, 132 headers=self.__get_headers(), 133 timeout=(5, 30), 134 ) 135 if response.status_code in [401, 403]: 136 raise api_exceptions.AuthorizationError 137 response.raise_for_status() 138 job_data = response.json() 139 try: 140 job = OqtantJob(**job_data) 141 except (KeyError, ValidationError) as err: 142 raise api_exceptions.ValidationError(f"Failed to get job {job_id}: {err}") 143 if not include_notes: 144 job.inputs[0].notes = "" 145 job = job.dict() 146 job.pop("input_count") 147 return job 148 149 def generate_oqtant_job(self, *, job: dict) -> OqtantJob: 150 """Generates an instance of OqtantJob from the provided dictionary that contains the 151 job details and input. Will validate the values and raise an informative error if 152 any violations are found. 153 Args: 154 job (dict): dictionary containing job details and input 155 Returns: 156 OqtantJob: an OqtantJob instance containing the details and input from the provided 157 dictionary 158 """ 159 try: 160 oqtant_job = OqtantJob(**job) 161 except ValidationError as err: 162 raise api_exceptions.ValidationError(f"Failed to generate OqtantJob: {err}") 163 return oqtant_job 164 165 def submit_job(self, *, job: OqtantJob) -> dict: 166 """Submits a single OqtantJob to the Oqtant REST API. Upon successful submission this 167 function will return a dictionary containing the external_id of the job and it's 168 position in the queue. 169 Args: 170 job (OqtantJob): the OqtantJob instance to submit for processing 171 Returns: 172 dict: dictionary containing the external_id of the job and it's queue position 173 """ 174 if not isinstance(job, OqtantJob): 175 try: 176 job = OqtantJob(**job) 177 except (TypeError, AttributeError, ValidationError) as err: 178 raise api_exceptions.ValidationError(f"Job is invalid: {err}") 179 data = { 180 "name": job.name, 181 "job_type": job.job_type, 182 "input_count": len(job.inputs), 183 "inputs": [input.dict() for input in job.inputs], 184 } 185 response = requests.post( 186 url=self.base_url, 187 json=data, 188 headers=self.__get_headers(), 189 timeout=(5, 30), 190 ) 191 if response.status_code in [401, 403]: 192 raise api_exceptions.AuthorizationError 193 response.raise_for_status() 194 response_data = response.json() 195 return response_data 196 197 def run_jobs( 198 self, 199 job_list: list[OqtantJob], 200 track_status: bool = False, 201 write: bool = False, 202 filename: str | list[str] = "", 203 ) -> list[str]: 204 """Submits a list of OqtantJobs to the Oqtant REST API. This function provides some 205 optional functionality to alter how it behaves. Providing it with an argument of 206 track_status=True will make it wait and poll the Oqtant REST API until all jobs 207 in the list have completed. The track_status functionality outputs each jobs 208 current status as it is polling and opens up the ability to use the other optional 209 arguments write and filename. The write and filename arguments enable the ability 210 to have the results of each completed job written to a file. The value of filename 211 is optional and if not provided will cause the files to be created using the 212 external_id of each job. If running more than one job and using the filename 213 argument it is required that the number of jobs in job_list match the number of 214 values in filename. 215 Args: 216 job_list (list[OqtantJob]): the list of OqtantJob instances to submit for processing 217 track_status (bool): optional argument to tell this function to either return 218 immediately or wait and poll until all jobs have completed 219 write (bool): optional argument to tell this function to write the results of each 220 job to file when complete 221 filename (Union[str, list[str]]): optional argument to be used in conjunction with the 222 write argument. allows the caller to customize the name(s) of the files being created 223 Returns: 224 list[str]: list of the external_id(s) returned for each submitted job in job_list 225 """ 226 if len(job_list) > self.max_job_batch_size: 227 raise AttributeError( 228 f"Maximum number of jobs submitted per run is {self.max_job_batch_size}." 229 ) 230 pending_jobs = [] 231 submitted_jobs = [] 232 for job in job_list: 233 response = self.submit_job(job=job) 234 external_id = response["job_id"] 235 queue_position = response["queue_position"] 236 est_time = response["est_time"] 237 238 job.external_id = external_id 239 self.active_jobs[external_id] = job 240 241 pending_jobs.append(external_id) 242 submitted_jobs.append(external_id) 243 print( 244 f"submitted {job.name} ID: {job.external_id} queue_position: {queue_position} \ 245 est. time: {est_time} minutes" 246 ) 247 print("Jobs submitted: \n") 248 if track_status: 249 self.track_jobs(pending_jobs=pending_jobs, filename=filename, write=write) 250 return submitted_jobs 251 252 def search_jobs( 253 self, 254 *, 255 job_type: job_schema.JobType | None = None, 256 name: job_schema.JobName | None = None, 257 submit_start: str | None = None, 258 submit_end: str | None = None, 259 notes: str | None = None, 260 ) -> list[dict]: 261 """Submits a query to the Oqtant REST API to search for jobs that match the provided criteria. 262 The search results will be limited to jobs that meet your Oqtant account access. 263 Args: 264 job_type (job_schema.JobType): the type of the jobs to search for 265 name (job_schema.JobName): the name of the job to search for 266 submit_start (str): the earliest submit date of the jobs to search for 267 submit_start (str): the latest submit date of the jobs to search for 268 notes (str): the notes of the jobs to search for 269 Returns: 270 list[dict]: a list of jobs matching the provided search criteria 271 """ 272 request_url = f"{self.base_url}/" 273 params = {} 274 for param in ["job_type", "name", "submit_start", "submit_end", "notes"]: 275 if locals()[param] is not None: 276 params[param] = locals()[param] 277 278 response = requests.get( 279 url=request_url, 280 params=params, 281 headers=self.__get_headers(), 282 timeout=(5, 30), 283 ) 284 if response.status_code in [401, 403]: 285 raise api_exceptions.AuthorizationError 286 response.raise_for_status() 287 job_data = response.json() 288 return job_data["items"] 289 290 def track_jobs( 291 self, 292 *, 293 pending_jobs: list[str], 294 filename: str | list = "", 295 write: bool = False, 296 ) -> None: 297 """Polls the Oqtant REST API with a list of job external_ids and waits until all of them have 298 completed. Will output each job's status while it is polling and will output a message when 299 all jobs have completed. This function provides some optional functionality to alter how it 300 behaves. Providing it with an argument of write will have it write the results of each 301 completed job to a file. There is an additional argument that can be used with write called 302 filename. The value of filename is optional and if not provided will cause the files to be 303 created using the external_id of each job. If tracking more than one job and using the 304 filename argument it is required that the number of jobs in job_list match the number of 305 values in filename. 306 Args: 307 pending_jobs (list[str]): list of job external_ids to track 308 write (bool): optional argument to tell this function to write the results of each job to 309 file when complete 310 filename (Union[str, list[str]]): optional argument to be used in conjunction with the write 311 argument. allows the caller to customize the name(s) of the files being created 312 """ 313 if write: 314 if not filename: 315 job_filenames = [f"{external_id}.txt" for external_id in pending_jobs] 316 elif isinstance(filename, str): 317 job_filenames = [filename] 318 else: 319 job_filenames = filename 320 if len(job_filenames) != len(pending_jobs): 321 raise AttributeError( 322 "Write filename list length does not match number of jobs." 323 ) 324 325 status = None 326 while pending_jobs: 327 for pending_job in pending_jobs: 328 job = self.get_job(job_id=pending_job) 329 self.active_jobs[pending_job] = job 330 if job.status != status: 331 print(f"job {pending_job} is in status {job.status}") 332 status = job.status 333 if job.status == job_schema.JobStatus.COMPLETE: 334 print(f"job complete: {pending_job}") 335 pending_jobs.remove(pending_job) 336 if write: 337 self._write_job_to_file( 338 self.active_jobs[pending_job], job_filenames.pop(0) 339 ) 340 if job.status in [ 341 job_schema.JobStatus.INCOMPLETE, 342 job_schema.JobStatus.FAILED, 343 ]: 344 pending_jobs.remove(pending_job) 345 if write: 346 job_filenames.pop(0) 347 time.sleep(2) 348 print("all jobs complete") 349 350 def load_job_from_id_list(self, job_id_list: list[str]) -> None: 351 """Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list 352 of job external_ids. The results of the jobs loaded by this function are limited to their 353 first run. 354 Args: 355 job_id_list (list[str]): list of job external_ids to load 356 """ 357 for job_id in job_id_list: 358 self.load_job_from_id(job_id) 359 360 def load_job_from_id(self, job_id: str, run: int = 1) -> None: 361 """Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a job 362 external_id. The results of the jobs loaded by this function can be targeted to a specific 363 run if there are multiple. 364 Args: 365 job_id (str): the external_id of the job to load 366 run (int): optional argument to target a specific job run 367 """ 368 try: 369 job = self.get_job(job_id=job_id, run=run) 370 self.active_jobs[job_id] = job 371 print(f"Loaded job: {job.name} {job_id}") 372 except Exception as err: 373 raise api_exceptions.ValidationError( 374 f"Failed to fetch job {job_id}: {err}. Please contact ColdQuanta if error persists" 375 ) 376 377 def load_job_from_file_list(self, file_list: list[str]) -> None: 378 """Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list 379 of filenames containing OqtantJob info. The results of the jobs loaded by this function are 380 limited to their first run. 381 Args: 382 file_list (list[str]): list of filenames containing OqtantJob information 383 """ 384 for f in file_list: 385 self.load_job_from_file(f) 386 387 def load_job_from_file(self, file: str) -> None: 388 """Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a file 389 containing OqtantJob info. The results of the jobs loaded by this function are limited to 390 their first run. 391 Args: 392 file_list (list[str]): list of filenames containing OqtantJob information 393 """ 394 try: 395 with open(file) as json_file: 396 data = json.load(json_file) 397 self.load_job_from_id(data["external_id"]) 398 except (FileNotFoundError, Exception) as err: 399 raise api_exceptions.JobReadError(f"Failed to load job from {file}: {err}") 400 401 def see_active_jobs(self, refresh: bool = True) -> None: 402 """Utility function to print out the current contents of the active_jobs list. The optional 403 argument of refresh tells the function whether it should refresh the data of pending or 404 running jobs stored in active_jobs before printing out the results. Refreshing also 405 updates the data in active_jobs so if jobs were submitted but not tracked this is a way 406 to check on their status. 407 Args: 408 refresh (bool): optional argument to refresh the data of jobs in active_jobs 409 """ 410 if refresh: 411 for external_id, job in self.active_jobs.items(): 412 if job.status in [ 413 job_schema.JobStatus.PENDING, 414 job_schema.JobStatus.RUNNING, 415 ]: 416 refreshed_job = self.get_job( 417 job_id=external_id, run=job.inputs[0].run 418 ) 419 self.active_jobs[external_id] = refreshed_job 420 print("ACTIVE JOBS") 421 print("NAME\t\tSTATUS\t\tTIME SUBMIT\t\tID") 422 print("_" * 50) 423 for job_id, job in self.active_jobs.items(): 424 print(f"{job.name}\t\t{job.status}\t\t{job.time_submit}\t\t{job_id}") 425 426 def _write_job_to_file(self, job: OqtantJob, filepath: str) -> None: 427 """Utility function to write an OqtantJob instance to a file. Requires the full filepath 428 including the name of the file the write. Files that already exist cannot be 429 overwritten. 430 Args: 431 job (OqtantJob): the OqtantJob instance to write to file 432 filepath (str): the full path to the file to write, including the name of the file 433 """ 434 if os.path.exists(filepath): 435 raise api_exceptions.JobWriteError( 436 "File already exists. Please choose a unique filename." 437 ) 438 439 try: 440 with open(filepath, "w") as f: 441 f.write(str(job.json())) 442 print(f"Wrote job {job.external_id} to file {filepath}") 443 except (FileNotFoundError, Exception) as err: 444 print(f"Failed to write job {job.external_id} to {filepath}: {err}") 445 446 def get_job_limits(self) -> dict: 447 """Utility method to get job limits from the Oqtant REST API 448 Returns: 449 dict: dictionary of job limits 450 """ 451 try: 452 token_data = jwt.decode( 453 self.token, key=None, options={"verify_signature": False} 454 ) 455 external_user_id = token_data["sub"] 456 except Exception: 457 raise api_exceptions.ValidationError( 458 "Unable to decode JWT token. Please contact ColdQuanta." 459 ) 460 461 url = f"{self.base_url.replace('jobs', 'users')}/{external_user_id}/job_limits" 462 response = requests.get( 463 url=url, 464 headers=self.__get_headers(), 465 timeout=(5, 30), 466 ) 467 if response.status_code in [401, 403]: 468 raise api_exceptions.AuthorizationError("Unauthorized") 469 response.raise_for_status() 470 job_limits = response.json() 471 return job_limits
Python class for interacting with Oqtant This class contains tools for: - Accessing all of the functionality of the Oqtant Web App (https://oqtant.infleqtion.com) - BARRIER (Barrier Manipulator) jobs - BEC (Ultracold Matter) jobs - Building parameterized (i.e. optimization) experiments using OqtantJobs - Submitting and retrieving OqtantJob results How Oqtant works: 1.) Construct a single or list of OqtantJobs using 'generate_oqtant_job()' 2.) Run the single or list of OqtantJobs on the Oqtant hardware using 'run_jobs()' - There is a limit of 30 OqtantJobs per use of 'run_jobs()' 3.) As OqtantJobs are running, the results are automatically stored in 'active_jobs' - The OqtantJobs stored in 'active_jobs' are available until the python session ends 4.) If you choose to not track the status of OqtantJobs with 'run_jobs()' you can see the status of your session's active OqtantJobs with 'see_active_jobs()' 5.) To operate on jobs submitted in a previous session you can load them into your 'active_jobs' by using either 'load_job_from_id()' or 'load_job_from_file()' 6.) To analyze OqtantJob objects and use Oqtant's job analysis library reference the OqtantJob class documentation located in 'oqtant/job.py' Need help? Found a bug? Contact albert@infleqtion.com for support. Thank you!
59 def __init__(self, *, settings, token, debug: bool = False): 60 self.base_url: str = settings.base_url 61 self.active_jobs: dict[str, OqtantJob] = {} 62 self.token: str = token 63 self.max_ind_var: int = settings.max_ind_var 64 self.max_job_batch_size: int = settings.max_job_batch_size 65 self.debug: bool = debug 66 self.version = version("oqtant") 67 68 if not self.debug: 69 sys.tracebacklimit = 0
81 def get_job(self, job_id: str, run: int = 1) -> OqtantJob: 82 """Gets an OqtantJob from the Oqtant REST API. This will always be a targeted query 83 for a specific run. If the run is omitted then this will always return the first 84 run of the job. Will return results for any job regardless of it's status. 85 Args: 86 job_id (str): this is the external_id of the job to fetch 87 run (int): the run to target, this defaults to the first run if omitted 88 Returns: 89 OqtantJob: an OqtantJob instance with the values of the job queried 90 """ 91 request_url = f"{self.base_url}/{job_id}" 92 params = {"run": run} 93 response = requests.get( 94 url=request_url, 95 params=params, 96 headers=self.__get_headers(), 97 timeout=(5, 30), 98 ) 99 if response.status_code in [401, 403]: 100 raise api_exceptions.AuthorizationError 101 response.raise_for_status() 102 job_data = response.json() 103 try: 104 job = OqtantJob(**job_data) 105 except (KeyError, ValidationError) as err: 106 raise api_exceptions.ValidationError(f"Failed to get job {job_id}: {err}") 107 return job
Gets an OqtantJob from the Oqtant REST API. This will always be a targeted query for a specific run. If the run is omitted then this will always return the first run of the job. Will return results for any job regardless of it's status. Args: job_id (str): this is the external_id of the job to fetch run (int): the run to target, this defaults to the first run if omitted Returns: OqtantJob: an OqtantJob instance with the values of the job queried
109 def get_job_inputs_without_output( 110 self, job_id: str, run: int | None = None, include_notes: bool = False 111 ) -> dict: 112 """Gets an OqtantJob from the Oqtant REST API. This can return all runs within a job 113 or a single run based on whether a run value is provided. The OqtantJobs returned 114 will be converted to dictionaries and will not have any output data, even if 115 they are complete. This is useful for taking an existing job and creating a new one 116 based on it's input data. 117 Args: 118 job_id (str): this is the external_id of the job to fetch 119 run (Union[int, None]): optional argument if caller wishes to only has a single run returned 120 include_notes (bool): optional argument if caller wishes to include any notes associated 121 with OqtantJob inputs. Defaults to False is not provided 122 Returns: 123 dict: a dict representation of an OqtantJob instance 124 """ 125 request_url = f"{self.base_url}/{job_id}" 126 params = {"exclude_input_output": True} 127 if run: 128 params["run"] = run 129 response = requests.get( 130 url=request_url, 131 params=params, 132 headers=self.__get_headers(), 133 timeout=(5, 30), 134 ) 135 if response.status_code in [401, 403]: 136 raise api_exceptions.AuthorizationError 137 response.raise_for_status() 138 job_data = response.json() 139 try: 140 job = OqtantJob(**job_data) 141 except (KeyError, ValidationError) as err: 142 raise api_exceptions.ValidationError(f"Failed to get job {job_id}: {err}") 143 if not include_notes: 144 job.inputs[0].notes = "" 145 job = job.dict() 146 job.pop("input_count") 147 return job
Gets an OqtantJob from the Oqtant REST API. This can return all runs within a job or a single run based on whether a run value is provided. The OqtantJobs returned will be converted to dictionaries and will not have any output data, even if they are complete. This is useful for taking an existing job and creating a new one based on it's input data. Args: job_id (str): this is the external_id of the job to fetch run (Union[int, None]): optional argument if caller wishes to only has a single run returned include_notes (bool): optional argument if caller wishes to include any notes associated with OqtantJob inputs. Defaults to False is not provided Returns: dict: a dict representation of an OqtantJob instance
149 def generate_oqtant_job(self, *, job: dict) -> OqtantJob: 150 """Generates an instance of OqtantJob from the provided dictionary that contains the 151 job details and input. Will validate the values and raise an informative error if 152 any violations are found. 153 Args: 154 job (dict): dictionary containing job details and input 155 Returns: 156 OqtantJob: an OqtantJob instance containing the details and input from the provided 157 dictionary 158 """ 159 try: 160 oqtant_job = OqtantJob(**job) 161 except ValidationError as err: 162 raise api_exceptions.ValidationError(f"Failed to generate OqtantJob: {err}") 163 return oqtant_job
Generates an instance of OqtantJob from the provided dictionary that contains the job details and input. Will validate the values and raise an informative error if any violations are found. Args: job (dict): dictionary containing job details and input Returns: OqtantJob: an OqtantJob instance containing the details and input from the provided dictionary
165 def submit_job(self, *, job: OqtantJob) -> dict: 166 """Submits a single OqtantJob to the Oqtant REST API. Upon successful submission this 167 function will return a dictionary containing the external_id of the job and it's 168 position in the queue. 169 Args: 170 job (OqtantJob): the OqtantJob instance to submit for processing 171 Returns: 172 dict: dictionary containing the external_id of the job and it's queue position 173 """ 174 if not isinstance(job, OqtantJob): 175 try: 176 job = OqtantJob(**job) 177 except (TypeError, AttributeError, ValidationError) as err: 178 raise api_exceptions.ValidationError(f"Job is invalid: {err}") 179 data = { 180 "name": job.name, 181 "job_type": job.job_type, 182 "input_count": len(job.inputs), 183 "inputs": [input.dict() for input in job.inputs], 184 } 185 response = requests.post( 186 url=self.base_url, 187 json=data, 188 headers=self.__get_headers(), 189 timeout=(5, 30), 190 ) 191 if response.status_code in [401, 403]: 192 raise api_exceptions.AuthorizationError 193 response.raise_for_status() 194 response_data = response.json() 195 return response_data
Submits a single OqtantJob to the Oqtant REST API. Upon successful submission this function will return a dictionary containing the external_id of the job and it's position in the queue. Args: job (OqtantJob): the OqtantJob instance to submit for processing Returns: dict: dictionary containing the external_id of the job and it's queue position
197 def run_jobs( 198 self, 199 job_list: list[OqtantJob], 200 track_status: bool = False, 201 write: bool = False, 202 filename: str | list[str] = "", 203 ) -> list[str]: 204 """Submits a list of OqtantJobs to the Oqtant REST API. This function provides some 205 optional functionality to alter how it behaves. Providing it with an argument of 206 track_status=True will make it wait and poll the Oqtant REST API until all jobs 207 in the list have completed. The track_status functionality outputs each jobs 208 current status as it is polling and opens up the ability to use the other optional 209 arguments write and filename. The write and filename arguments enable the ability 210 to have the results of each completed job written to a file. The value of filename 211 is optional and if not provided will cause the files to be created using the 212 external_id of each job. If running more than one job and using the filename 213 argument it is required that the number of jobs in job_list match the number of 214 values in filename. 215 Args: 216 job_list (list[OqtantJob]): the list of OqtantJob instances to submit for processing 217 track_status (bool): optional argument to tell this function to either return 218 immediately or wait and poll until all jobs have completed 219 write (bool): optional argument to tell this function to write the results of each 220 job to file when complete 221 filename (Union[str, list[str]]): optional argument to be used in conjunction with the 222 write argument. allows the caller to customize the name(s) of the files being created 223 Returns: 224 list[str]: list of the external_id(s) returned for each submitted job in job_list 225 """ 226 if len(job_list) > self.max_job_batch_size: 227 raise AttributeError( 228 f"Maximum number of jobs submitted per run is {self.max_job_batch_size}." 229 ) 230 pending_jobs = [] 231 submitted_jobs = [] 232 for job in job_list: 233 response = self.submit_job(job=job) 234 external_id = response["job_id"] 235 queue_position = response["queue_position"] 236 est_time = response["est_time"] 237 238 job.external_id = external_id 239 self.active_jobs[external_id] = job 240 241 pending_jobs.append(external_id) 242 submitted_jobs.append(external_id) 243 print( 244 f"submitted {job.name} ID: {job.external_id} queue_position: {queue_position} \ 245 est. time: {est_time} minutes" 246 ) 247 print("Jobs submitted: \n") 248 if track_status: 249 self.track_jobs(pending_jobs=pending_jobs, filename=filename, write=write) 250 return submitted_jobs
Submits a list of OqtantJobs to the Oqtant REST API. This function provides some optional functionality to alter how it behaves. Providing it with an argument of track_status=True will make it wait and poll the Oqtant REST API until all jobs in the list have completed. The track_status functionality outputs each jobs current status as it is polling and opens up the ability to use the other optional arguments write and filename. The write and filename arguments enable the ability to have the results of each completed job written to a file. The value of filename is optional and if not provided will cause the files to be created using the external_id of each job. If running more than one job and using the filename argument it is required that the number of jobs in job_list match the number of values in filename. Args: job_list (list[OqtantJob]): the list of OqtantJob instances to submit for processing track_status (bool): optional argument to tell this function to either return immediately or wait and poll until all jobs have completed write (bool): optional argument to tell this function to write the results of each job to file when complete filename (Union[str, list[str]]): optional argument to be used in conjunction with the write argument. allows the caller to customize the name(s) of the files being created Returns: list[str]: list of the external_id(s) returned for each submitted job in job_list
252 def search_jobs( 253 self, 254 *, 255 job_type: job_schema.JobType | None = None, 256 name: job_schema.JobName | None = None, 257 submit_start: str | None = None, 258 submit_end: str | None = None, 259 notes: str | None = None, 260 ) -> list[dict]: 261 """Submits a query to the Oqtant REST API to search for jobs that match the provided criteria. 262 The search results will be limited to jobs that meet your Oqtant account access. 263 Args: 264 job_type (job_schema.JobType): the type of the jobs to search for 265 name (job_schema.JobName): the name of the job to search for 266 submit_start (str): the earliest submit date of the jobs to search for 267 submit_start (str): the latest submit date of the jobs to search for 268 notes (str): the notes of the jobs to search for 269 Returns: 270 list[dict]: a list of jobs matching the provided search criteria 271 """ 272 request_url = f"{self.base_url}/" 273 params = {} 274 for param in ["job_type", "name", "submit_start", "submit_end", "notes"]: 275 if locals()[param] is not None: 276 params[param] = locals()[param] 277 278 response = requests.get( 279 url=request_url, 280 params=params, 281 headers=self.__get_headers(), 282 timeout=(5, 30), 283 ) 284 if response.status_code in [401, 403]: 285 raise api_exceptions.AuthorizationError 286 response.raise_for_status() 287 job_data = response.json() 288 return job_data["items"]
Submits a query to the Oqtant REST API to search for jobs that match the provided criteria. The search results will be limited to jobs that meet your Oqtant account access. Args: job_type (job_schema.JobType): the type of the jobs to search for name (job_schema.JobName): the name of the job to search for submit_start (str): the earliest submit date of the jobs to search for submit_start (str): the latest submit date of the jobs to search for notes (str): the notes of the jobs to search for Returns: list[dict]: a list of jobs matching the provided search criteria
290 def track_jobs( 291 self, 292 *, 293 pending_jobs: list[str], 294 filename: str | list = "", 295 write: bool = False, 296 ) -> None: 297 """Polls the Oqtant REST API with a list of job external_ids and waits until all of them have 298 completed. Will output each job's status while it is polling and will output a message when 299 all jobs have completed. This function provides some optional functionality to alter how it 300 behaves. Providing it with an argument of write will have it write the results of each 301 completed job to a file. There is an additional argument that can be used with write called 302 filename. The value of filename is optional and if not provided will cause the files to be 303 created using the external_id of each job. If tracking more than one job and using the 304 filename argument it is required that the number of jobs in job_list match the number of 305 values in filename. 306 Args: 307 pending_jobs (list[str]): list of job external_ids to track 308 write (bool): optional argument to tell this function to write the results of each job to 309 file when complete 310 filename (Union[str, list[str]]): optional argument to be used in conjunction with the write 311 argument. allows the caller to customize the name(s) of the files being created 312 """ 313 if write: 314 if not filename: 315 job_filenames = [f"{external_id}.txt" for external_id in pending_jobs] 316 elif isinstance(filename, str): 317 job_filenames = [filename] 318 else: 319 job_filenames = filename 320 if len(job_filenames) != len(pending_jobs): 321 raise AttributeError( 322 "Write filename list length does not match number of jobs." 323 ) 324 325 status = None 326 while pending_jobs: 327 for pending_job in pending_jobs: 328 job = self.get_job(job_id=pending_job) 329 self.active_jobs[pending_job] = job 330 if job.status != status: 331 print(f"job {pending_job} is in status {job.status}") 332 status = job.status 333 if job.status == job_schema.JobStatus.COMPLETE: 334 print(f"job complete: {pending_job}") 335 pending_jobs.remove(pending_job) 336 if write: 337 self._write_job_to_file( 338 self.active_jobs[pending_job], job_filenames.pop(0) 339 ) 340 if job.status in [ 341 job_schema.JobStatus.INCOMPLETE, 342 job_schema.JobStatus.FAILED, 343 ]: 344 pending_jobs.remove(pending_job) 345 if write: 346 job_filenames.pop(0) 347 time.sleep(2) 348 print("all jobs complete")
Polls the Oqtant REST API with a list of job external_ids and waits until all of them have completed. Will output each job's status while it is polling and will output a message when all jobs have completed. This function provides some optional functionality to alter how it behaves. Providing it with an argument of write will have it write the results of each completed job to a file. There is an additional argument that can be used with write called filename. The value of filename is optional and if not provided will cause the files to be created using the external_id of each job. If tracking more than one job and using the filename argument it is required that the number of jobs in job_list match the number of values in filename. Args: pending_jobs (list[str]): list of job external_ids to track write (bool): optional argument to tell this function to write the results of each job to file when complete filename (Union[str, list[str]]): optional argument to be used in conjunction with the write argument. allows the caller to customize the name(s) of the files being created
350 def load_job_from_id_list(self, job_id_list: list[str]) -> None: 351 """Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list 352 of job external_ids. The results of the jobs loaded by this function are limited to their 353 first run. 354 Args: 355 job_id_list (list[str]): list of job external_ids to load 356 """ 357 for job_id in job_id_list: 358 self.load_job_from_id(job_id)
Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list of job external_ids. The results of the jobs loaded by this function are limited to their first run. Args: job_id_list (list[str]): list of job external_ids to load
360 def load_job_from_id(self, job_id: str, run: int = 1) -> None: 361 """Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a job 362 external_id. The results of the jobs loaded by this function can be targeted to a specific 363 run if there are multiple. 364 Args: 365 job_id (str): the external_id of the job to load 366 run (int): optional argument to target a specific job run 367 """ 368 try: 369 job = self.get_job(job_id=job_id, run=run) 370 self.active_jobs[job_id] = job 371 print(f"Loaded job: {job.name} {job_id}") 372 except Exception as err: 373 raise api_exceptions.ValidationError( 374 f"Failed to fetch job {job_id}: {err}. Please contact ColdQuanta if error persists" 375 )
Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a job external_id. The results of the jobs loaded by this function can be targeted to a specific run if there are multiple. Args: job_id (str): the external_id of the job to load run (int): optional argument to target a specific job run
377 def load_job_from_file_list(self, file_list: list[str]) -> None: 378 """Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list 379 of filenames containing OqtantJob info. The results of the jobs loaded by this function are 380 limited to their first run. 381 Args: 382 file_list (list[str]): list of filenames containing OqtantJob information 383 """ 384 for f in file_list: 385 self.load_job_from_file(f)
Loads OqtantJobs from the Oqtant REST API into the current active_jobs list using a list of filenames containing OqtantJob info. The results of the jobs loaded by this function are limited to their first run. Args: file_list (list[str]): list of filenames containing OqtantJob information
387 def load_job_from_file(self, file: str) -> None: 388 """Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a file 389 containing OqtantJob info. The results of the jobs loaded by this function are limited to 390 their first run. 391 Args: 392 file_list (list[str]): list of filenames containing OqtantJob information 393 """ 394 try: 395 with open(file) as json_file: 396 data = json.load(json_file) 397 self.load_job_from_id(data["external_id"]) 398 except (FileNotFoundError, Exception) as err: 399 raise api_exceptions.JobReadError(f"Failed to load job from {file}: {err}")
Loads an OqtantJob from the Oqtant REST API into the current active_jobs list using a file containing OqtantJob info. The results of the jobs loaded by this function are limited to their first run. Args: file_list (list[str]): list of filenames containing OqtantJob information
401 def see_active_jobs(self, refresh: bool = True) -> None: 402 """Utility function to print out the current contents of the active_jobs list. The optional 403 argument of refresh tells the function whether it should refresh the data of pending or 404 running jobs stored in active_jobs before printing out the results. Refreshing also 405 updates the data in active_jobs so if jobs were submitted but not tracked this is a way 406 to check on their status. 407 Args: 408 refresh (bool): optional argument to refresh the data of jobs in active_jobs 409 """ 410 if refresh: 411 for external_id, job in self.active_jobs.items(): 412 if job.status in [ 413 job_schema.JobStatus.PENDING, 414 job_schema.JobStatus.RUNNING, 415 ]: 416 refreshed_job = self.get_job( 417 job_id=external_id, run=job.inputs[0].run 418 ) 419 self.active_jobs[external_id] = refreshed_job 420 print("ACTIVE JOBS") 421 print("NAME\t\tSTATUS\t\tTIME SUBMIT\t\tID") 422 print("_" * 50) 423 for job_id, job in self.active_jobs.items(): 424 print(f"{job.name}\t\t{job.status}\t\t{job.time_submit}\t\t{job_id}")
Utility function to print out the current contents of the active_jobs list. The optional argument of refresh tells the function whether it should refresh the data of pending or running jobs stored in active_jobs before printing out the results. Refreshing also updates the data in active_jobs so if jobs were submitted but not tracked this is a way to check on their status. Args: refresh (bool): optional argument to refresh the data of jobs in active_jobs
446 def get_job_limits(self) -> dict: 447 """Utility method to get job limits from the Oqtant REST API 448 Returns: 449 dict: dictionary of job limits 450 """ 451 try: 452 token_data = jwt.decode( 453 self.token, key=None, options={"verify_signature": False} 454 ) 455 external_user_id = token_data["sub"] 456 except Exception: 457 raise api_exceptions.ValidationError( 458 "Unable to decode JWT token. Please contact ColdQuanta." 459 ) 460 461 url = f"{self.base_url.replace('jobs', 'users')}/{external_user_id}/job_limits" 462 response = requests.get( 463 url=url, 464 headers=self.__get_headers(), 465 timeout=(5, 30), 466 ) 467 if response.status_code in [401, 403]: 468 raise api_exceptions.AuthorizationError("Unauthorized") 469 response.raise_for_status() 470 job_limits = response.json() 471 return job_limits
Utility method to get job limits from the Oqtant REST API Returns: dict: dictionary of job limits
474def version_check(client_version: str) -> None: 475 """Compares the given current Oqtant version with the version currently on pypi, 476 and raises a warning if it is older. 477 Args: 478 client_version (str): the client semver version number 479 """ 480 resp = requests.get("https://pypi.org/pypi/oqtant/json", timeout=5) 481 if resp.status_code == 200: 482 current_version = resp.json()["info"]["version"] 483 if semver.compare(client_version, current_version) < 0: 484 warnings.warn( 485 f"Please upgrade to Oqtant version {current_version}. You are currently using version {client_version}." 486 )
Compares the given current Oqtant version with the version currently on pypi, and raises a warning if it is older. Args: client_version (str): the client semver version number
489def get_oqtant_client(token: str) -> OqtantClient: 490 """A utility function to create a new OqtantClient instance. 491 Args: 492 token (str): the auth0 token required for interacting with the Oqtant REST API 493 Returns: 494 OqtantClient: authenticated instance of OqtantClient 495 """ 496 497 client = OqtantClient(settings=settings, token=token) 498 version_check(client.version) 499 return client
A utility function to create a new OqtantClient instance. Args: token (str): the auth0 token required for interacting with the Oqtant REST API Returns: OqtantClient: authenticated instance of OqtantClient