diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index 6482780ffa..21486f10f8 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -45,17 +45,20 @@ def __init__( ) self._api = Runtime(self._session) - def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: + def list_programs( + self, search: str = "", limit: int = None, skip: int = None + ) -> Dict[str, Any]: """Return a list of runtime programs. Args: + search: Returns programs containing search word in name or description. limit: The number of programs to return. skip: The number of programs to skip. Returns: A list of runtime programs. """ - return self._api.list_programs(limit, skip) + return self._api.list_programs(search, limit, skip) def program_create( self, diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index 42442c9b3f..94842bb986 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -56,10 +56,13 @@ def program_job(self, job_id: str) -> "ProgramJob": """ return ProgramJob(self.session, job_id) - def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: + def list_programs( + self, search: str = "", limit: int = None, skip: int = None + ) -> Dict[str, Any]: """Return a list of runtime programs. Args: + search: Returns programs containing search word in name or description. limit: The number of programs to return. skip: The number of programs to skip. @@ -67,7 +70,9 @@ def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: A list of runtime programs. """ url = self.get_url("programs") - payload: Dict[str, int] = {} + payload: Dict[str, Union[int, str]] = {} + if search: + payload["search"] = search if limit: payload["limit"] = limit if skip: diff --git a/qiskit_ibm_runtime/ibm_runtime_service.py b/qiskit_ibm_runtime/ibm_runtime_service.py index 0af7ae805e..22894311a3 100644 --- a/qiskit_ibm_runtime/ibm_runtime_service.py +++ b/qiskit_ibm_runtime/ibm_runtime_service.py @@ -641,6 +641,7 @@ def pprint_programs( self, refresh: bool = False, detailed: bool = False, + search: Optional[str] = "", limit: int = 20, skip: int = 0, ) -> None: @@ -650,11 +651,12 @@ def pprint_programs( refresh: If ``True``, re-query the server for the programs. Otherwise return the cached value. detailed: If ``True`` print all details about available runtime programs. + search: Returns programs containing search word in name or description. limit: The number of programs returned at a time. Default and maximum value of 20. skip: The number of programs to skip. """ - programs = self.programs(refresh, limit, skip) + programs = self.programs(refresh, search, limit, skip) for prog in programs: print("=" * 50) if detailed: @@ -667,7 +669,11 @@ def pprint_programs( print(f" Description: {prog.description}") def programs( - self, refresh: bool = False, limit: int = 20, skip: int = 0 + self, + refresh: bool = False, + search: Optional[str] = "", + limit: int = 20, + skip: int = 0, ) -> List[RuntimeProgram]: """Return available runtime programs. @@ -676,35 +682,30 @@ def programs( Args: refresh: If ``True``, re-query the server for the programs. Otherwise return the cached value. + search: Returns programs containing search word in name or description. limit: The number of programs returned at a time. ``None`` means no limit. skip: The number of programs to skip. Returns: A list of runtime programs. """ + already_retrieved = False if skip is None: skip = 0 - if not self._programs or refresh: - self._programs = {} - current_page_limit = 20 - offset = 0 - while True: - response = self._api_client.list_programs( - limit=current_page_limit, skip=offset - ) - program_page = response.get("programs", []) - # count is the total number of programs that would be returned if - # there was no limit or skip - count = response.get("count", 0) - for prog_dict in program_page: - program = self._to_program(prog_dict) - self._programs[program.program_id] = program - if len(self._programs) == count: - # Stop if there are no more programs returned by the server. - break - offset += len(program_page) + if not self._programs or (refresh and not search): + self._programs = self._retrieve_programs() + already_retrieved = True if limit is None: limit = len(self._programs) + if search: + matched_programs = [] + if refresh and not already_retrieved: + matched_programs = list(self._retrieve_programs(search).values()) + else: + for program in list(self._programs.values()): + if search in program.name or search in program.description: + matched_programs.append(program) + return matched_programs[skip : limit + skip] return list(self._programs.values())[skip : limit + skip] def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: @@ -738,6 +739,35 @@ def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: return self._programs[program_id] + def _retrieve_programs(self, search: str = "") -> Dict[str, RuntimeProgram]: + """Make an API call to fetch programs. + + Args: + search: Search query for program name and description. + + Returns: + A dict of ``RuntimeProgram`` instances, keyed by program name. + """ + programs = {} + current_page_limit = 20 + offset = 0 + while True: + response = self._api_client.list_programs( + search=search, limit=current_page_limit, skip=offset + ) + program_page = response.get("programs", []) + # count is the total number of programs that would be returned if + # there was no limit or skip + count = response.get("count", 0) + for prog_dict in program_page: + program = self._to_program(prog_dict) + programs[program.program_id] = program + if len(programs) == count: + # Stop if there are no more programs returned by the server. + break + offset += len(program_page) + return programs + def _to_program(self, response: Dict) -> RuntimeProgram: """Convert server response to ``RuntimeProgram`` instances. diff --git a/releasenotes/notes/program-search-query-675455e48d0a5f0a.yaml b/releasenotes/notes/program-search-query-675455e48d0a5f0a.yaml new file mode 100644 index 0000000000..caf421749c --- /dev/null +++ b/releasenotes/notes/program-search-query-675455e48d0a5f0a.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The ``search`` parameter has been added to + :meth:`qiskit_ibm_runtime.IBMRuntimeService.programs` and + :meth:`qiskit_ibm_runtime.IBMRuntimeService.pprint_programs` + which can be used to query programs containing the + search word in its name or description. \ No newline at end of file diff --git a/test/mock/fake_runtime_client.py b/test/mock/fake_runtime_client.py index a80f875af9..68434567f6 100644 --- a/test/mock/fake_runtime_client.py +++ b/test/mock/fake_runtime_client.py @@ -271,11 +271,17 @@ def set_final_status(self, final_status): """Set job status to passed in final status instantly.""" self._final_status = final_status - def list_programs(self, limit, skip): + def list_programs(self, search, limit, skip): """List all programs.""" programs = [] for prog in self._programs.values(): - programs.append(prog.to_dict()) + if not search: + programs.append(prog.to_dict()) + if ( + search in prog.to_dict()["name"].lower() + or search in prog.to_dict()["description"].lower() + ): + programs.append(prog.to_dict()) return {"programs": programs[skip : limit + skip], "count": len(self._programs)} def program_create( diff --git a/test/test_integration_program.py b/test/test_integration_program.py index f39e5efdbc..7cc52b3f42 100644 --- a/test/test_integration_program.py +++ b/test/test_integration_program.py @@ -34,7 +34,7 @@ class TestIntegrationProgram(IBMIntegrationTestCase): def test_list_programs(self, service): """Test listing programs.""" program_id = self._upload_program(service) - programs = service.programs() + programs = service.programs(refresh=True) self.assertTrue(programs) found = False for prog in programs: @@ -58,6 +58,17 @@ def test_list_programs_with_limit_skip(self, service): self.assertIn(all_ids[1], some_ids) self.assertIn(all_ids[2], some_ids) + @run_integration_test + def test_filter_programs_with_search(self, service): + """Test filtering programs with the search parameter""" + program_id = self._upload_program(service) + programs = service.programs(search="test program") + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_id, all_ids) + programs = service.programs(search="test sample") + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_id, all_ids) + @run_integration_test def test_list_program(self, service): """Test listing a single program.""" diff --git a/test/test_programs.py b/test/test_programs.py index 4f63e5358e..aca009ca64 100644 --- a/test/test_programs.py +++ b/test/test_programs.py @@ -55,6 +55,17 @@ def test_list_programs_with_limit_skip(self, service): all_ids = [prog.program_id for prog in programs] self.assertIn(program_ids[0], all_ids) + @run_legacy_and_cloud_fake + def test_filter_programs_with_search(self, service): + """Test filtering programs with the search parameter""" + program_id = upload_program(service) + programs = service.programs(search="Test program") + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_id, all_ids) + programs = service.programs(search="Test sample") + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_id, all_ids) + @run_legacy_and_cloud_fake def test_list_program(self, service): """Test listing a single program."""