diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index c2eaed835..38ef6fea2 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -179,7 +179,10 @@ def jobs_get( limit: int = None, skip: int = None, pending: bool = None, - program_id: str = None + program_id: str = None, + hub: str = None, + group: str = None, + project: str = None ) -> Dict: """Get job data for all jobs. @@ -189,11 +192,15 @@ def jobs_get( pending: Returns 'QUEUED' and 'RUNNING' jobs if True, returns 'DONE', 'CANCELLED' and 'ERROR' jobs if False. program_id: Filter by Program ID. + hub: Filter by hub - hub, group, and project must all be specified. + group: Filter by group - hub, group, and project must all be specified. + project: Filter by project - hub, group, and project must all be specified. Returns: JSON response. """ - return self.api.jobs_get(limit=limit, skip=skip, pending=pending, program_id=program_id) + return self.api.jobs_get(limit=limit, skip=skip, pending=pending, + program_id=program_id, hub=hub, group=group, project=project) def job_results(self, job_id: str) -> str: """Get the results of a program job. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 5340c9298..2a68744cb 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -139,7 +139,10 @@ def jobs_get( limit: int = None, skip: int = None, pending: bool = None, - program_id: str = None + program_id: str = None, + hub: str = None, + group: str = None, + project: str = None ) -> Dict: """Get a list of job data. @@ -149,6 +152,9 @@ def jobs_get( pending: Returns 'QUEUED' and 'RUNNING' jobs if True, returns 'DONE', 'CANCELLED' and 'ERROR' jobs if False. program_id: Filter by Program ID. + hub: Filter by hub - hub, group, and project must all be specified. + group: Filter by group - hub, group, and project must all be specified. + project: Filter by project - hub, group, and project must all be specified. Returns: JSON response. @@ -163,6 +169,8 @@ def jobs_get( payload['pending'] = 'true' if pending else 'false' if program_id: payload['program'] = program_id + if all([hub, group, project]): + payload['provider'] = f"{hub}/{group}/{project}" return self.session.get(url, params=payload).json() def logout(self) -> None: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 4db69021f..e6eda3d57 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -526,7 +526,10 @@ def jobs( limit: Optional[int] = 10, skip: int = 0, pending: bool = None, - program_id: str = None + program_id: str = None, + hub: str = None, + group: str = None, + project: str = None ) -> List[RuntimeJob]: """Retrieve all runtime jobs, subject to optional filtering. @@ -537,10 +540,22 @@ def jobs( jobs are included. If ``False``, 'DONE', 'CANCELLED' and 'ERROR' jobs are included. program_id: Filter by Program ID. + hub: Filter by hub - hub, group, and project must all be specified. + group: Filter by group - hub, group, and project must all be specified. + project: Filter by project - hub, group, and project must all be specified. Returns: A list of runtime jobs. + + Raises: + IBMInputValueError: If any but not all of the parameters ``hub``, ``group`` + and ``project`` are given. """ + if any([hub, group, project]) and not all([hub, group, project]): + raise IBMInputValueError('Hub, group and project ' + 'parameters must all be specified. ' + 'hub = "{}", group = "{}", project = "{}"' + .format(hub, group, project)) job_responses = [] # type: List[Dict[str, Any]] current_page_limit = limit or 20 offset = skip @@ -550,7 +565,10 @@ def jobs( limit=current_page_limit, skip=offset, pending=pending, - program_id=program_id) + program_id=program_id, + hub=hub, + group=group, + project=project) job_page = jobs_response["jobs"] # count is the total number of jobs that would be returned if # there was no limit or skip diff --git a/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml b/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml new file mode 100644 index 000000000..61e28343b --- /dev/null +++ b/releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can now pass ``hub``, ``group``, and ``project`` parameters to + :meth:`qiskit_ibm.runtime.IBMRuntimeService.jobs` to filter jobs. \ No newline at end of file diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index a4b4d2607..3a4ffd6eb 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -214,13 +214,23 @@ def _auto_progress(self): class BaseFakeRuntimeClient: """Base class for faking the runtime client.""" - def __init__(self, job_classes=None, final_status=None, job_kwargs=None): + def __init__(self, job_classes=None, final_status=None, job_kwargs=None, + hub=None, group=None, project=None): """Initialize a fake runtime client.""" self._programs = {} self._jobs = {} self._job_classes = job_classes or [] self._final_status = final_status self._job_kwargs = job_kwargs or {} + self._hub = hub + self._group = group + self._project = project + + def set_hgp(self, hub, group, project): + """Set hub, group and project""" + self._hub = hub + self._group = group + self._project = project def set_job_classes(self, classes): """Set job classes to use.""" @@ -297,9 +307,12 @@ def program_run( """Run the specified program.""" job_id = uuid.uuid4().hex job_cls = self._job_classes.pop(0) if len(self._job_classes) > 0 else BaseFakeRuntimeJob + hub = self._hub or credentials.hub + group = self._group or credentials.group + project = self._project or credentials.project job = job_cls(job_id=job_id, program_id=program_id, - hub=credentials.hub, group=credentials.group, - project=credentials.project, backend_name=backend_name, + hub=hub, group=group, + project=project, backend_name=backend_name, params=params, final_status=self._final_status, image=image, **self._job_kwargs) self._jobs[job_id] = job @@ -315,7 +328,8 @@ def job_get(self, job_id): """Get the specific job.""" return self._get_job(job_id).to_dict() - def jobs_get(self, limit=None, skip=None, pending=None, program_id=None): + def jobs_get(self, limit=None, skip=None, pending=None, program_id=None, + hub=None, group=None, project=None): """Get all jobs.""" pending_statuses = ['QUEUED', 'RUNNING'] returned_statuses = ['COMPLETED', 'FAILED', 'CANCELLED'] @@ -330,6 +344,10 @@ def jobs_get(self, limit=None, skip=None, pending=None, program_id=None): if program_id: jobs = [job for job in jobs if job._program_id == program_id] count = len(jobs) + if all([hub, group, project]): + jobs = [job for job in jobs if + job._hub == hub and job._group == group and job._project == project] + count = len(jobs) jobs = jobs[skip:limit+skip] return {"jobs": [job.to_dict() for job in jobs], "count": count} diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index ac2e0a174..810d1a5b7 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -622,6 +622,22 @@ def test_jobs_filter_by_program_id(self): self.assertEqual(program_id, rjobs[0].program_id) self.assertEqual(1, len(rjobs)) + def test_jobs_filter_by_provider(self): + """Test retrieving jobs by provider.""" + program_id = self._upload_program() + job = self._run_program(program_id=program_id, + hub="defaultHub", group="defaultGroup", project="defaultProject") + job.wait_for_final_state() + rjobs = self.runtime.jobs(program_id=program_id, + hub="defaultHub", group="defaultGroup", project="defaultProject") + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + rjobs = self.runtime.jobs(program_id=program_id, + hub="test", group="test", project="test") + self.assertFalse(rjobs) + with self.assertRaises(IBMInputValueError): + self.runtime.jobs(hub="defaultHub") + def test_cancel_job(self): """Test canceling a job.""" job = self._run_program(job_classes=CancelableRuntimeJob) @@ -725,13 +741,15 @@ def _upload_program(self, name=None, max_execution_time=300, return program_id def _run_program(self, program_id=None, inputs=None, job_classes=None, final_status=None, - decoder=None, image=""): + decoder=None, image="", hub=None, group=None, project=None): """Run a program.""" options = {'backend_name': "some_backend"} if final_status is not None: self.runtime._api_client.set_final_status(final_status) elif job_classes: self.runtime._api_client.set_job_classes(job_classes) + elif all([hub, group, project]): + self.runtime._api_client.set_hgp(hub, group, project) if program_id is None: program_id = self._upload_program() job = self.runtime.run(program_id=program_id, inputs=inputs, diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 9aab64d9c..40dd08c76 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -395,6 +395,22 @@ def test_retrieve_jobs_by_program_id(self): self.assertEqual(program_id, rjobs[0].program_id) self.assertEqual(1, len(rjobs)) + def test_jobs_filter_by_provider(self): + """Test retrieving jobs by provider.""" + hub = self.provider.credentials.hub + group = self.provider.credentials.group + project = self.provider.credentials.project + program_id = self._upload_program() + job = self._run_program(program_id=program_id) + job.wait_for_final_state() + rjobs = self.provider.runtime.jobs(program_id=program_id, + hub=hub, group=group, project=project) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + rjobs = self.provider.runtime.jobs(program_id=program_id, + hub="test", group="test", project="test") + self.assertFalse(rjobs) + def test_cancel_job_queued(self): """Test canceling a queued job.""" _ = self._run_program(iterations=10)