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 Oraqle
 37    This class contains tools for:
 38        - Accessing all of the functionality of the Oraqle Web App (https://oraqle-dev.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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle REST API to search for jobs that match the provided criteria.
261           The search results will be limited to jobs that meet your Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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
settings = Settings(auth0_base_url='https://coldquanta-dev.us.auth0.com', auth0_client_id='ZzQdn5ZZq1dmpP5N55KINr33u47RBRiu', auth0_scope='offline_access bec_dev_service:client', auth0_audience='https://oraqle-dev.infleqtion.com/oqtant', signin_local_callback_url='http://localhost:8080', base_url='https://oraqle-dev.infleqtion.com/api/jobs', max_ind_var=2, max_job_batch_size=30)
class OqtantClient:
 36class OqtantClient:
 37    """Python class for interacting with Oraqle
 38    This class contains tools for:
 39        - Accessing all of the functionality of the Oraqle Web App (https://oraqle-dev.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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle REST API to search for jobs that match the provided criteria.
262           The search results will be limited to jobs that meet your Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle This class contains tools for: - Accessing all of the functionality of the Oraqle Web App (https://oraqle-dev.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 Oraqle 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!

OqtantClient(*, settings, token, debug: bool = False)
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
base_url: str
active_jobs: dict[str, oqtant.schemas.job.OqtantJob]
token: str
max_ind_var: int
max_job_batch_size: int
debug: bool
version
def get_job(self, job_id: str, run: int = 1) -> oqtant.schemas.job.OqtantJob:
 81    def get_job(self, job_id: str, run: int = 1) -> OqtantJob:
 82        """Gets an OqtantJob from the Oraqle 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 Oraqle 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

def get_job_inputs_without_output( self, job_id: str, run: int | None = None, include_notes: bool = False) -> dict:
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 Oraqle 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 Oraqle 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

def generate_oqtant_job(self, *, job: dict) -> oqtant.schemas.job.OqtantJob:
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

def submit_job(self, *, job: oqtant.schemas.job.OqtantJob) -> dict:
165    def submit_job(self, *, job: OqtantJob) -> dict:
166        """Submits a single OqtantJob to the Oraqle 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 Oraqle 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

def run_jobs( self, job_list: list, track_status: bool = False, write: bool = False, filename: str | list[str] = '') -> list[str]:
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 Oraqle 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 Oraqle 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 Oraqle 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 Oraqle 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

def search_jobs( self, *, job_type: bert_schemas.job.JobType | None = None, name: bert_schemas.job.JobName | None = None, submit_start: str | None = None, submit_end: str | None = None, notes: str | None = None) -> list[dict]:
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 Oraqle REST API to search for jobs that match the provided criteria.
262           The search results will be limited to jobs that meet your Oraqle 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 Oraqle REST API to search for jobs that match the provided criteria. The search results will be limited to jobs that meet your Oraqle 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

def track_jobs( self, *, pending_jobs: list, filename: str | list = '', write: bool = False) -> None:
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 Oraqle 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 Oraqle 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

def load_job_from_id_list(self, job_id_list: list) -> None:
350    def load_job_from_id_list(self, job_id_list: list[str]) -> None:
351        """Loads OqtantJobs from the Oraqle 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 Oraqle 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

def load_job_from_id(self, job_id: str, run: int = 1) -> None:
360    def load_job_from_id(self, job_id: str, run: int = 1) -> None:
361        """Loads an OqtantJob from the Oraqle 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 Oraqle 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

def load_job_from_file_list(self, file_list: list) -> None:
377    def load_job_from_file_list(self, file_list: list[str]) -> None:
378        """Loads OqtantJobs from the Oraqle 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 Oraqle 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

def load_job_from_file(self, file: str) -> None:
387    def load_job_from_file(self, file: str) -> None:
388        """Loads an OqtantJob from the Oraqle 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 Oraqle 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

def see_active_jobs(self, refresh: bool = True) -> None:
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

def get_job_limits(self) -> dict:
446    def get_job_limits(self) -> dict:
447        """Utility method to get job limits from the Oraqle 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 Oraqle REST API Returns: dict: dictionary of job limits

def version_check(client_version: str) -> None:
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

def get_oqtant_client(token: str) -> oqtant.oqtant_client.OqtantClient:
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 Oraqle 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 Oraqle REST API Returns: OqtantClient: authenticated instance of OqtantClient