oqtant.oqtant_client API documentation

# oqtant.oqtant_client

View Source

      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

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://oqtant.infleqtion.com/oqtant', signin_local_callback_url='http://localhost:8080', base_url='https://oqtant.infleqtion.com/api/jobs', max_ind_var=2, max_job_batch_size=30)
class OqtantClient: View Source

     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!

OqtantClient(*, settings, token, debug: bool = False) View Source

    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: View Source

     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

def get_job_inputs_without_output( self, job_id: str, run: int | None = None, include_notes: bool = False) -> dict: View Source

    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

def generate_oqtant_job(self, *, job: dict) -> oqtant.schemas.job.OqtantJob: View Source

    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: View Source

    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

def run_jobs( self, job_list: list, track_status: bool = False, write: bool = False, filename: str | list[str] = '') -> list[str]: View Source

    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

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]: View Source

    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

def track_jobs( self, *, pending_jobs: list, filename: str | list = '', write: bool = False) -> None: View Source

    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

def load_job_from_id_list(self, job_id_list: list) -> None: View Source

    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

def load_job_from_id(self, job_id: str, run: int = 1) -> None: View Source

    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

def load_job_from_file_list(self, file_list: list) -> None: View Source

    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

def load_job_from_file(self, file: str) -> None: View Source

    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

def see_active_jobs(self, refresh: bool = True) -> None: View Source

    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: View Source

    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

def version_check(client_version: str) -> None: View Source

    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: View Source

    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