From 464192f939f4279dbe5e851dc489a3bfac49d65e Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 01:14:35 -0400 Subject: [PATCH 01/48] Remove version field from runtime program (#152) * Remove version field from runtime program * Add release note --- qiskit_ibm/api/clients/runtime.py | 4 +--- qiskit_ibm/api/rest/runtime.py | 4 ---- qiskit_ibm/runtime/__init__.py | 7 ++----- qiskit_ibm/runtime/ibm_runtime_service.py | 7 ++----- .../runtime/program/program_metadata_sample.json | 1 - qiskit_ibm/runtime/runtime_program.py | 14 -------------- .../remove-version-field-0543061d4a7e059a.yaml | 4 ++++ test/ibm/runtime/fake_runtime_client.py | 8 +++----- test/ibm/runtime/test_runtime.py | 9 +++------ test/ibm/runtime/test_runtime_integration.py | 1 - 10 files changed, 15 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 4156c4999..05b8264d0 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -54,7 +54,6 @@ def program_create( description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, backend_requirements: Optional[Dict] = None, parameters: Optional[Dict] = None, return_values: Optional[List] = None, @@ -68,7 +67,6 @@ def program_create( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. backend_requirements: Backend requirements. parameters: Program parameters. return_values: Program return values. @@ -81,7 +79,7 @@ def program_create( program_data=program_data, name=name, description=description, max_execution_time=max_execution_time, - is_public=is_public, version=version, backend_requirements=backend_requirements, + is_public=is_public, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results ) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index e8252ee0b..da8cf3487 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -70,7 +70,6 @@ def create_program( description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, backend_requirements: Optional[Dict] = None, parameters: Optional[Dict] = None, return_values: Optional[List] = None, @@ -84,7 +83,6 @@ def create_program( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. backend_requirements: Backend requirements. parameters: Program parameters. return_values: Program return values. @@ -99,8 +97,6 @@ def create_program( 'description': description.encode(), 'max_execution_time': max_execution_time, 'isPublic': is_public} - if version is not None: - data['version'] = version if backend_requirements: data['backendRequirements'] = json.dumps(backend_requirements) if parameters: diff --git a/qiskit_ibm/runtime/__init__.py b/qiskit_ibm/runtime/__init__.py index 83012f6e2..7b8fc84aa 100644 --- a/qiskit_ibm/runtime/__init__.py +++ b/qiskit_ibm/runtime/__init__.py @@ -191,14 +191,11 @@ def interim_result_callback(job_id, interim_result): provider = IBMProvider() program_id = provider.runtime.upload_program( data="my_vqe.py", - metadata="my_vqe_metadata.json", - version="1.2" + metadata="my_vqe_metadata.json" ) In the example above, the file ``my_vqe.py`` contains the program data, and -``my_vqe_metadata.json`` contains the program metadata. An additional -parameter ``version`` is also specified, which takes precedence over any -``version`` value specified in ``my_vqe_metadata.json``. +``my_vqe_metadata.json`` contains the program metadata. Method :meth:`IBMRuntimeService.delete_program` allows you to delete a program. diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 5d10f1d0e..febe967f6 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -195,7 +195,6 @@ def _to_program(self, response: Dict) -> RuntimeProgram: interim_results=interim_results, max_execution_time=response.get('cost', 0), creation_date=response.get('creationDate', ""), - version=response.get('version', "0"), backend_requirements=backend_req, is_public=response.get('isPublic', False)) @@ -272,7 +271,6 @@ def upload_program( is_public: Optional[bool] = False, max_execution_time: Optional[int] = None, description: Optional[str] = None, - version: Optional[float] = None, backend_requirements: Optional[str] = None, parameters: Optional[List[ProgramParameter]] = None, return_values: Optional[List[ProgramResult]] = None, @@ -306,7 +304,6 @@ def upload_program( not specified via `metadata`. is_public: Whether the runtime program should be visible to the public. description: Program description. Required if not specified via `metadata`. - version: Program version. The default is 1.0 if not specified. backend_requirements: Backend requirements. parameters: A list of program input parameters. return_values: A list of program return values. @@ -326,7 +323,7 @@ def upload_program( metadata=metadata, name=name, max_execution_time=max_execution_time, is_public=is_public, description=description, - version=version, backend_requirements=backend_requirements, + backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results) @@ -385,7 +382,7 @@ def _merge_metadata( initial[key] = val # TODO validate metadata format - metadata_keys = ['name', 'max_execution_time', 'description', 'version', + metadata_keys = ['name', 'max_execution_time', 'description', 'backend_requirements', 'parameters', 'return_values', 'interim_results', 'is_public'] return {key: val for key, val in initial.items() if key in metadata_keys} diff --git a/qiskit_ibm/runtime/program/program_metadata_sample.json b/qiskit_ibm/runtime/program/program_metadata_sample.json index a38c18fd6..5df24385f 100644 --- a/qiskit_ibm/runtime/program/program_metadata_sample.json +++ b/qiskit_ibm/runtime/program/program_metadata_sample.json @@ -2,7 +2,6 @@ "name": "runtime-simple", "description": "Simple runtime program used for testing.", "max_execution_time": 300, - "version": 1.0, "backend_requirements": {"min_num_qubits": 5}, "parameters": [ {"name": "iterations", "description": "Number of iterations to run. Each iteration generates a runs a random circuit.", "type": "integer", "required": true} diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index ee884146b..bdb503d46 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -51,7 +51,6 @@ def __init__( return_values: Optional[List] = None, interim_results: Optional[List] = None, max_execution_time: int = 0, - version: str = "0", backend_requirements: Optional[Dict] = None, creation_date: str = "", is_public: Optional[bool] = False @@ -66,7 +65,6 @@ def __init__( return_values: Documentation on program return values. interim_results: Documentation on program interim results. max_execution_time: Maximum execution time. - version: Program version. backend_requirements: Backend requirements. creation_date: Program creation date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. @@ -75,7 +73,6 @@ def __init__( self._id = program_id self._description = description self._max_execution_time = max_execution_time - self._version = version self._backend_requirements = backend_requirements or {} self._parameters: List[ProgramParameter] = [] self._return_values: List[ProgramResult] = [] @@ -114,7 +111,6 @@ def _format_common(items: List) -> None: formatted = [f'{self.program_id}:', f" Name: {self.name}", f" Description: {self.description}", - f" Version: {self.version}", f" Creation date: {self.creation_date}", f" Max execution time: {self.max_execution_time}", f" Input parameters:"] @@ -148,7 +144,6 @@ def to_dict(self) -> Dict: "name": self.name, "description": self.description, "max_execution_time": self.max_execution_time, - "version": self.version, "backend_requirements": self.backend_requirements, "parameters": self.parameters(), "return_values": self.return_values, @@ -227,15 +222,6 @@ def max_execution_time(self) -> int: """ return self._max_execution_time - @property - def version(self) -> str: - """Program version. - - Returns: - Program version. - """ - return self._version - @property def backend_requirements(self) -> Dict: """Backend requirements. diff --git a/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml new file mode 100644 index 000000000..099dcd222 --- /dev/null +++ b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Runtime programs will no longer have a ``version`` field. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 6711d5ee7..a041c718a 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -26,7 +26,7 @@ class BaseFakeProgram: """Base class for faking a program.""" - def __init__(self, program_id, name, data, cost, description, version="1.0", + def __init__(self, program_id, name, data, cost, description, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Initialize a fake program.""" @@ -35,7 +35,6 @@ def __init__(self, program_id, name, data, cost, description, version="1.0", self._data = data self._cost = cost self._description = description - self._version = version self._backend_requirements = backend_requirements self._parameters = parameters self._return_values = return_values @@ -48,7 +47,6 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'version': self._version, 'isPublic': self._is_public} if include_data: out['data'] = self._data @@ -237,7 +235,7 @@ def list_programs(self): programs.append(prog.to_dict()) return programs - def program_create(self, program_data, name, description, max_execution_time, version="1.0", + def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Create a program.""" @@ -249,7 +247,7 @@ def program_create(self, program_data, name, description, max_execution_time, ve raise RequestsApiError("Program already exists.", status_code=409) self._programs[program_id] = BaseFakeProgram( program_id=program_id, name=name, data=program_data, cost=max_execution_time, - description=description, version=version, backend_requirements=backend_requirements, + description=description, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results, is_public=is_public) return {'id': program_id} diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index f60f027fb..b918eab08 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -70,7 +70,6 @@ class TestRuntime(IBMTestCase): "name": "qiskit-test", "description": "Test program.", "max_execution_time": 300, - "version": "0.1", "backend_requirements": {"min_num_qubits": 5}, "parameters": [ {'name': 'param1', 'description': 'Desc 1', 'type': 'str', 'required': True}, @@ -286,13 +285,13 @@ def test_print_programs(self): for prog in programs: self.assertIn(prog.program_id, stdout) self.assertIn(prog.name, stdout) - self.assertNotIn(prog.version, stdout) + self.assertNotIn(str(prog.max_execution_time), stdout) self.runtime.pprint_programs(detailed=True) stdout_detailed = mock_stdout.getvalue() for prog in programs: self.assertIn(prog.program_id, stdout_detailed) self.assertIn(prog.name, stdout_detailed) - self.assertIn(prog.version, stdout_detailed) + self.assertIn(str(prog.max_execution_time), stdout_detailed) def test_upload_program(self): """Test uploading a program.""" @@ -600,7 +599,6 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) - self.assertEqual(self.DEFAULT_METADATA["version"], program.version) self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], program.backend_requirements) self.assertEqual([ProgramParameter(**param) for param in @@ -615,12 +613,11 @@ def test_program_metadata(self): def test_metadata_combined(self): """Test combining metadata""" - update_metadata = {"version": "1.2", "max_execution_time": 600} + update_metadata = {"max_execution_time": 600} program_id = self.runtime.upload_program( data="def main() {}", metadata=self.DEFAULT_METADATA, **update_metadata) program = self.runtime.program(program_id) self.assertEqual(update_metadata['max_execution_time'], program.max_execution_time) - self.assertEqual(update_metadata["version"], program.version) def test_different_providers(self): """Test retrieving job submitted with different provider.""" diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index de8559e23..b08f4f059 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -613,7 +613,6 @@ def _validate_program(self, program): self.assertTrue(program.description) self.assertTrue(program.max_execution_time) self.assertTrue(program.creation_date) - self.assertTrue(program.version) def _upload_program( self, From abb98d09a7e8e92716f874f39b9b3c644f691127 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 01:24:53 -0400 Subject: [PATCH 02/48] Rename isPublic to is_public when creating or reading runtime programs (#155) --- qiskit_ibm/api/rest/runtime.py | 2 +- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index da8cf3487..41e14765a 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -96,7 +96,7 @@ def create_program( 'cost': str(max_execution_time), 'description': description.encode(), 'max_execution_time': max_execution_time, - 'isPublic': is_public} + 'is_public': is_public} if backend_requirements: data['backendRequirements'] = json.dumps(backend_requirements) if parameters: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index febe967f6..776b8347d 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -196,7 +196,7 @@ def _to_program(self, response: Dict) -> RuntimeProgram: max_execution_time=response.get('cost', 0), creation_date=response.get('creationDate', ""), backend_requirements=backend_req, - is_public=response.get('isPublic', False)) + is_public=response.get('is_public', False)) def run( self, diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index a041c718a..1243fbb9a 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -47,7 +47,7 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'isPublic': self._is_public} + 'is_public': self._is_public} if include_data: out['data'] = self._data if self._backend_requirements: From 31fdd4b1ae807c7beca9f8d256e86fbd0c7da18b Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Mon, 18 Oct 2021 10:16:48 -0500 Subject: [PATCH 03/48] Update programId to program_id when running program (#139) This needs to change in the program upload body request in order to meet the IBM Cloud API guidance. Co-authored-by: Jessie Yu --- qiskit_ibm/api/rest/runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 41e14765a..1adcc04cb 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -136,7 +136,7 @@ def program_run( """ url = self.get_url('jobs') payload = { - 'programId': program_id, + 'program_id': program_id, 'hub': hub, 'group': group, 'project': project, From 0d5ebf86e9cb54056e2a6a54d33f64dd853f19c5 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 11:30:48 -0400 Subject: [PATCH 04/48] Add support to view program update date (#160) --- qiskit_ibm/runtime/ibm_runtime_service.py | 3 ++- qiskit_ibm/runtime/runtime_program.py | 13 +++++++++++++ ...eature-program-update-date-7325797d7abd36ad.yaml | 5 +++++ test/ibm/runtime/fake_runtime_client.py | 4 +++- test/ibm/runtime/test_runtime.py | 2 ++ 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 776b8347d..cc552493e 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -194,7 +194,8 @@ def _to_program(self, response: Dict) -> RuntimeProgram: return_values=ret_vals, interim_results=interim_results, max_execution_time=response.get('cost', 0), - creation_date=response.get('creationDate', ""), + creation_date=response.get('creation_date', ""), + update_date=response.get('update_date', ""), backend_requirements=backend_req, is_public=response.get('is_public', False)) diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index bdb503d46..9b6cde746 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -53,6 +53,7 @@ def __init__( max_execution_time: int = 0, backend_requirements: Optional[Dict] = None, creation_date: str = "", + update_date: str = "", is_public: Optional[bool] = False ) -> None: """RuntimeProgram constructor. @@ -67,6 +68,7 @@ def __init__( max_execution_time: Maximum execution time. backend_requirements: Backend requirements. creation_date: Program creation date. + update_date: Program last updated date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. """ self._name = program_name @@ -78,6 +80,7 @@ def __init__( self._return_values: List[ProgramResult] = [] self._interim_results: List[ProgramResult] = [] self._creation_date = creation_date + self._update_date = update_date self._is_public = is_public if parameters: @@ -112,6 +115,7 @@ def _format_common(items: List) -> None: f" Name: {self.name}", f" Description: {self.description}", f" Creation date: {self.creation_date}", + f" Update date: {self.update_date}", f" Max execution time: {self.max_execution_time}", f" Input parameters:"] @@ -240,6 +244,15 @@ def creation_date(self) -> str: """ return self._creation_date + @property + def update_date(self) -> str: + """Program last updated date. + + Returns: + Program last updated date. + """ + return self._update_date + @property def is_public(self) -> bool: """Whether the program is visible to all. diff --git a/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml new file mode 100644 index 000000000..6fe46c1df --- /dev/null +++ b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can view the last updated date of a runtime program using + :attr:`~qiskit_ibm.runtime.RuntimeProgram.update_date` property. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 1243fbb9a..f5990abed 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -47,7 +47,9 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'is_public': self._is_public} + 'is_public': self._is_public, + 'creation_date': '2021-09-13T17:27:42Z', + 'update_date': '2021-09-14T19:25:32Z'} if include_data: out['data'] = self._data if self._backend_requirements: diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index b918eab08..acedbea2f 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -599,6 +599,8 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], program.backend_requirements) self.assertEqual([ProgramParameter(**param) for param in From 767afebf4f23edcbc4d3f69af37629a3bbf338f4 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 22:39:30 -0400 Subject: [PATCH 05/48] Upload runtime program using 'data' field (#157) --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 9 ++++----- qiskit_ibm/runtime/ibm_runtime_service.py | 4 +++- test/ibm/runtime/fake_runtime_client.py | 3 --- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 05b8264d0..d13136efb 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -49,7 +49,7 @@ def list_programs(self) -> List[Dict]: def program_create( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, @@ -63,7 +63,7 @@ def program_create( Args: name: Name of the program. - program_data: Program data. + program_data: Program data (base64 encoded). description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 1adcc04cb..e04cacbda 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -65,7 +65,7 @@ def list_programs(self) -> List[Dict]: def create_program( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, @@ -78,7 +78,7 @@ def create_program( """Upload a new program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). name: Name of the program. description: Program description. max_execution_time: Maximum execution time. @@ -93,6 +93,7 @@ def create_program( """ url = self.get_url('programs') data = {'name': name, + 'data': program_data, 'cost': str(max_execution_time), 'description': description.encode(), 'max_execution_time': max_execution_time, @@ -105,9 +106,7 @@ def create_program( data['returnValues'] = json.dumps(return_values) if interim_results: data['interimResults'] = json.dumps(interim_results) - - files = {'program': (name, program_data)} # type: ignore[dict-item] - response = self.session.post(url, data=data, files=files).json() + response = self.session.post(url, data=data).json() return response def program_run( diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index cc552493e..ab21ec87e 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -12,6 +12,7 @@ """Qiskit runtime service.""" +import base64 import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json @@ -338,7 +339,8 @@ def upload_program( data = file.read() try: - response = self._api_client.program_create(program_data=data.encode(), + program_data = base64.b64encode(data.encode('utf-8')).decode('utf-8') + response = self._api_client.program_create(program_data=program_data, **program_metadata) except RequestsApiError as ex: if ex.status_code == 409: diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index f5990abed..65b433987 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -241,9 +241,6 @@ def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Create a program.""" - if isinstance(program_data, str): - with open(program_data, 'rb') as file: - program_data = file.read() program_id = name if program_id in self._programs: raise RequestsApiError("Program already exists.", status_code=409) From 24dfa95df1f3737bdd2944a31aca255a947e8e36 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 22:41:26 -0400 Subject: [PATCH 06/48] Read programs from "programs" array in response (#161) --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 2 +- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index d13136efb..4aae5de02 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -13,7 +13,7 @@ """Client for accessing IBM Quantum runtime service.""" import logging -from typing import List, Dict, Union, Optional +from typing import Any, List, Dict, Union, Optional from qiskit_ibm.credentials import Credentials from qiskit_ibm.api.session import RetrySession @@ -39,7 +39,7 @@ def __init__( **credentials.connection_parameters()) self.api = Runtime(self._session) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index e04cacbda..8ae956dc2 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -54,7 +54,7 @@ def program_job(self, job_id: str) -> 'ProgramJob': """ return ProgramJob(self.session, job_id) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index ab21ec87e..185df03dd 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -140,7 +140,7 @@ def programs(self, refresh: bool = False) -> List[RuntimeProgram]: if not self._programs or refresh: self._programs = {} response = self._api_client.list_programs() - for prog_dict in response: + for prog_dict in response.get("programs", []): program = self._to_program(prog_dict) self._programs[program.program_id] = program return list(self._programs.values()) diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 65b433987..3a05e45f9 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -235,7 +235,7 @@ def list_programs(self): programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) - return programs + return {"programs": programs} def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, From 79cd9bd0e5dfc07fb1c902ba0146db2075cbfaf4 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 01:14:35 -0400 Subject: [PATCH 07/48] Remove version field from runtime program (#152) * Remove version field from runtime program * Add release note --- qiskit_ibm/api/clients/runtime.py | 4 +--- qiskit_ibm/api/rest/runtime.py | 4 ---- qiskit_ibm/runtime/__init__.py | 7 ++----- qiskit_ibm/runtime/ibm_runtime_service.py | 7 ++----- .../runtime/program/program_metadata_sample.json | 1 - qiskit_ibm/runtime/runtime_program.py | 14 -------------- .../remove-version-field-0543061d4a7e059a.yaml | 4 ++++ test/ibm/runtime/fake_runtime_client.py | 8 +++----- test/ibm/runtime/test_runtime.py | 9 +++------ test/ibm/runtime/test_runtime_integration.py | 1 - 10 files changed, 15 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 4156c4999..05b8264d0 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -54,7 +54,6 @@ def program_create( description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, backend_requirements: Optional[Dict] = None, parameters: Optional[Dict] = None, return_values: Optional[List] = None, @@ -68,7 +67,6 @@ def program_create( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. backend_requirements: Backend requirements. parameters: Program parameters. return_values: Program return values. @@ -81,7 +79,7 @@ def program_create( program_data=program_data, name=name, description=description, max_execution_time=max_execution_time, - is_public=is_public, version=version, backend_requirements=backend_requirements, + is_public=is_public, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results ) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index e8252ee0b..da8cf3487 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -70,7 +70,6 @@ def create_program( description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, backend_requirements: Optional[Dict] = None, parameters: Optional[Dict] = None, return_values: Optional[List] = None, @@ -84,7 +83,6 @@ def create_program( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. backend_requirements: Backend requirements. parameters: Program parameters. return_values: Program return values. @@ -99,8 +97,6 @@ def create_program( 'description': description.encode(), 'max_execution_time': max_execution_time, 'isPublic': is_public} - if version is not None: - data['version'] = version if backend_requirements: data['backendRequirements'] = json.dumps(backend_requirements) if parameters: diff --git a/qiskit_ibm/runtime/__init__.py b/qiskit_ibm/runtime/__init__.py index 83012f6e2..7b8fc84aa 100644 --- a/qiskit_ibm/runtime/__init__.py +++ b/qiskit_ibm/runtime/__init__.py @@ -191,14 +191,11 @@ def interim_result_callback(job_id, interim_result): provider = IBMProvider() program_id = provider.runtime.upload_program( data="my_vqe.py", - metadata="my_vqe_metadata.json", - version="1.2" + metadata="my_vqe_metadata.json" ) In the example above, the file ``my_vqe.py`` contains the program data, and -``my_vqe_metadata.json`` contains the program metadata. An additional -parameter ``version`` is also specified, which takes precedence over any -``version`` value specified in ``my_vqe_metadata.json``. +``my_vqe_metadata.json`` contains the program metadata. Method :meth:`IBMRuntimeService.delete_program` allows you to delete a program. diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 5d10f1d0e..febe967f6 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -195,7 +195,6 @@ def _to_program(self, response: Dict) -> RuntimeProgram: interim_results=interim_results, max_execution_time=response.get('cost', 0), creation_date=response.get('creationDate', ""), - version=response.get('version', "0"), backend_requirements=backend_req, is_public=response.get('isPublic', False)) @@ -272,7 +271,6 @@ def upload_program( is_public: Optional[bool] = False, max_execution_time: Optional[int] = None, description: Optional[str] = None, - version: Optional[float] = None, backend_requirements: Optional[str] = None, parameters: Optional[List[ProgramParameter]] = None, return_values: Optional[List[ProgramResult]] = None, @@ -306,7 +304,6 @@ def upload_program( not specified via `metadata`. is_public: Whether the runtime program should be visible to the public. description: Program description. Required if not specified via `metadata`. - version: Program version. The default is 1.0 if not specified. backend_requirements: Backend requirements. parameters: A list of program input parameters. return_values: A list of program return values. @@ -326,7 +323,7 @@ def upload_program( metadata=metadata, name=name, max_execution_time=max_execution_time, is_public=is_public, description=description, - version=version, backend_requirements=backend_requirements, + backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results) @@ -385,7 +382,7 @@ def _merge_metadata( initial[key] = val # TODO validate metadata format - metadata_keys = ['name', 'max_execution_time', 'description', 'version', + metadata_keys = ['name', 'max_execution_time', 'description', 'backend_requirements', 'parameters', 'return_values', 'interim_results', 'is_public'] return {key: val for key, val in initial.items() if key in metadata_keys} diff --git a/qiskit_ibm/runtime/program/program_metadata_sample.json b/qiskit_ibm/runtime/program/program_metadata_sample.json index a38c18fd6..5df24385f 100644 --- a/qiskit_ibm/runtime/program/program_metadata_sample.json +++ b/qiskit_ibm/runtime/program/program_metadata_sample.json @@ -2,7 +2,6 @@ "name": "runtime-simple", "description": "Simple runtime program used for testing.", "max_execution_time": 300, - "version": 1.0, "backend_requirements": {"min_num_qubits": 5}, "parameters": [ {"name": "iterations", "description": "Number of iterations to run. Each iteration generates a runs a random circuit.", "type": "integer", "required": true} diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index ee884146b..bdb503d46 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -51,7 +51,6 @@ def __init__( return_values: Optional[List] = None, interim_results: Optional[List] = None, max_execution_time: int = 0, - version: str = "0", backend_requirements: Optional[Dict] = None, creation_date: str = "", is_public: Optional[bool] = False @@ -66,7 +65,6 @@ def __init__( return_values: Documentation on program return values. interim_results: Documentation on program interim results. max_execution_time: Maximum execution time. - version: Program version. backend_requirements: Backend requirements. creation_date: Program creation date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. @@ -75,7 +73,6 @@ def __init__( self._id = program_id self._description = description self._max_execution_time = max_execution_time - self._version = version self._backend_requirements = backend_requirements or {} self._parameters: List[ProgramParameter] = [] self._return_values: List[ProgramResult] = [] @@ -114,7 +111,6 @@ def _format_common(items: List) -> None: formatted = [f'{self.program_id}:', f" Name: {self.name}", f" Description: {self.description}", - f" Version: {self.version}", f" Creation date: {self.creation_date}", f" Max execution time: {self.max_execution_time}", f" Input parameters:"] @@ -148,7 +144,6 @@ def to_dict(self) -> Dict: "name": self.name, "description": self.description, "max_execution_time": self.max_execution_time, - "version": self.version, "backend_requirements": self.backend_requirements, "parameters": self.parameters(), "return_values": self.return_values, @@ -227,15 +222,6 @@ def max_execution_time(self) -> int: """ return self._max_execution_time - @property - def version(self) -> str: - """Program version. - - Returns: - Program version. - """ - return self._version - @property def backend_requirements(self) -> Dict: """Backend requirements. diff --git a/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml new file mode 100644 index 000000000..099dcd222 --- /dev/null +++ b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Runtime programs will no longer have a ``version`` field. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 6711d5ee7..a041c718a 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -26,7 +26,7 @@ class BaseFakeProgram: """Base class for faking a program.""" - def __init__(self, program_id, name, data, cost, description, version="1.0", + def __init__(self, program_id, name, data, cost, description, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Initialize a fake program.""" @@ -35,7 +35,6 @@ def __init__(self, program_id, name, data, cost, description, version="1.0", self._data = data self._cost = cost self._description = description - self._version = version self._backend_requirements = backend_requirements self._parameters = parameters self._return_values = return_values @@ -48,7 +47,6 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'version': self._version, 'isPublic': self._is_public} if include_data: out['data'] = self._data @@ -237,7 +235,7 @@ def list_programs(self): programs.append(prog.to_dict()) return programs - def program_create(self, program_data, name, description, max_execution_time, version="1.0", + def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Create a program.""" @@ -249,7 +247,7 @@ def program_create(self, program_data, name, description, max_execution_time, ve raise RequestsApiError("Program already exists.", status_code=409) self._programs[program_id] = BaseFakeProgram( program_id=program_id, name=name, data=program_data, cost=max_execution_time, - description=description, version=version, backend_requirements=backend_requirements, + description=description, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results, is_public=is_public) return {'id': program_id} diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index f60f027fb..b918eab08 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -70,7 +70,6 @@ class TestRuntime(IBMTestCase): "name": "qiskit-test", "description": "Test program.", "max_execution_time": 300, - "version": "0.1", "backend_requirements": {"min_num_qubits": 5}, "parameters": [ {'name': 'param1', 'description': 'Desc 1', 'type': 'str', 'required': True}, @@ -286,13 +285,13 @@ def test_print_programs(self): for prog in programs: self.assertIn(prog.program_id, stdout) self.assertIn(prog.name, stdout) - self.assertNotIn(prog.version, stdout) + self.assertNotIn(str(prog.max_execution_time), stdout) self.runtime.pprint_programs(detailed=True) stdout_detailed = mock_stdout.getvalue() for prog in programs: self.assertIn(prog.program_id, stdout_detailed) self.assertIn(prog.name, stdout_detailed) - self.assertIn(prog.version, stdout_detailed) + self.assertIn(str(prog.max_execution_time), stdout_detailed) def test_upload_program(self): """Test uploading a program.""" @@ -600,7 +599,6 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) - self.assertEqual(self.DEFAULT_METADATA["version"], program.version) self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], program.backend_requirements) self.assertEqual([ProgramParameter(**param) for param in @@ -615,12 +613,11 @@ def test_program_metadata(self): def test_metadata_combined(self): """Test combining metadata""" - update_metadata = {"version": "1.2", "max_execution_time": 600} + update_metadata = {"max_execution_time": 600} program_id = self.runtime.upload_program( data="def main() {}", metadata=self.DEFAULT_METADATA, **update_metadata) program = self.runtime.program(program_id) self.assertEqual(update_metadata['max_execution_time'], program.max_execution_time) - self.assertEqual(update_metadata["version"], program.version) def test_different_providers(self): """Test retrieving job submitted with different provider.""" diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 39a0cd2ff..34869db2a 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -626,7 +626,6 @@ def _validate_program(self, program): self.assertTrue(program.description) self.assertTrue(program.max_execution_time) self.assertTrue(program.creation_date) - self.assertTrue(program.version) def _upload_program( self, From 45ab0b228db4de2c8a3d95ddb94251d380643c4e Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 01:24:53 -0400 Subject: [PATCH 08/48] Rename isPublic to is_public when creating or reading runtime programs (#155) --- qiskit_ibm/api/rest/runtime.py | 2 +- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index da8cf3487..41e14765a 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -96,7 +96,7 @@ def create_program( 'cost': str(max_execution_time), 'description': description.encode(), 'max_execution_time': max_execution_time, - 'isPublic': is_public} + 'is_public': is_public} if backend_requirements: data['backendRequirements'] = json.dumps(backend_requirements) if parameters: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index febe967f6..776b8347d 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -196,7 +196,7 @@ def _to_program(self, response: Dict) -> RuntimeProgram: max_execution_time=response.get('cost', 0), creation_date=response.get('creationDate', ""), backend_requirements=backend_req, - is_public=response.get('isPublic', False)) + is_public=response.get('is_public', False)) def run( self, diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index a041c718a..1243fbb9a 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -47,7 +47,7 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'isPublic': self._is_public} + 'is_public': self._is_public} if include_data: out['data'] = self._data if self._backend_requirements: From faff4e25aff6f126ca12edff1412f00c69a6bf96 Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Mon, 18 Oct 2021 10:16:48 -0500 Subject: [PATCH 09/48] Update programId to program_id when running program (#139) This needs to change in the program upload body request in order to meet the IBM Cloud API guidance. Co-authored-by: Jessie Yu --- qiskit_ibm/api/rest/runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 41e14765a..1adcc04cb 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -136,7 +136,7 @@ def program_run( """ url = self.get_url('jobs') payload = { - 'programId': program_id, + 'program_id': program_id, 'hub': hub, 'group': group, 'project': project, From bcc27fbd4a3a9e1317a3673ae339ef4f397ef80c Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 11:30:48 -0400 Subject: [PATCH 10/48] Add support to view program update date (#160) --- qiskit_ibm/runtime/ibm_runtime_service.py | 3 ++- qiskit_ibm/runtime/runtime_program.py | 13 +++++++++++++ ...eature-program-update-date-7325797d7abd36ad.yaml | 5 +++++ test/ibm/runtime/fake_runtime_client.py | 4 +++- test/ibm/runtime/test_runtime.py | 2 ++ 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 776b8347d..cc552493e 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -194,7 +194,8 @@ def _to_program(self, response: Dict) -> RuntimeProgram: return_values=ret_vals, interim_results=interim_results, max_execution_time=response.get('cost', 0), - creation_date=response.get('creationDate', ""), + creation_date=response.get('creation_date', ""), + update_date=response.get('update_date', ""), backend_requirements=backend_req, is_public=response.get('is_public', False)) diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index bdb503d46..9b6cde746 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -53,6 +53,7 @@ def __init__( max_execution_time: int = 0, backend_requirements: Optional[Dict] = None, creation_date: str = "", + update_date: str = "", is_public: Optional[bool] = False ) -> None: """RuntimeProgram constructor. @@ -67,6 +68,7 @@ def __init__( max_execution_time: Maximum execution time. backend_requirements: Backend requirements. creation_date: Program creation date. + update_date: Program last updated date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. """ self._name = program_name @@ -78,6 +80,7 @@ def __init__( self._return_values: List[ProgramResult] = [] self._interim_results: List[ProgramResult] = [] self._creation_date = creation_date + self._update_date = update_date self._is_public = is_public if parameters: @@ -112,6 +115,7 @@ def _format_common(items: List) -> None: f" Name: {self.name}", f" Description: {self.description}", f" Creation date: {self.creation_date}", + f" Update date: {self.update_date}", f" Max execution time: {self.max_execution_time}", f" Input parameters:"] @@ -240,6 +244,15 @@ def creation_date(self) -> str: """ return self._creation_date + @property + def update_date(self) -> str: + """Program last updated date. + + Returns: + Program last updated date. + """ + return self._update_date + @property def is_public(self) -> bool: """Whether the program is visible to all. diff --git a/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml new file mode 100644 index 000000000..6fe46c1df --- /dev/null +++ b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can view the last updated date of a runtime program using + :attr:`~qiskit_ibm.runtime.RuntimeProgram.update_date` property. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 1243fbb9a..f5990abed 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -47,7 +47,9 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'is_public': self._is_public} + 'is_public': self._is_public, + 'creation_date': '2021-09-13T17:27:42Z', + 'update_date': '2021-09-14T19:25:32Z'} if include_data: out['data'] = self._data if self._backend_requirements: diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index b918eab08..acedbea2f 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -599,6 +599,8 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], program.backend_requirements) self.assertEqual([ProgramParameter(**param) for param in From 866d04538bd36448aa460bfa81921c74a3449b2c Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 22:39:30 -0400 Subject: [PATCH 11/48] Upload runtime program using 'data' field (#157) --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 9 ++++----- qiskit_ibm/runtime/ibm_runtime_service.py | 4 +++- test/ibm/runtime/fake_runtime_client.py | 3 --- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 05b8264d0..d13136efb 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -49,7 +49,7 @@ def list_programs(self) -> List[Dict]: def program_create( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, @@ -63,7 +63,7 @@ def program_create( Args: name: Name of the program. - program_data: Program data. + program_data: Program data (base64 encoded). description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 1adcc04cb..e04cacbda 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -65,7 +65,7 @@ def list_programs(self) -> List[Dict]: def create_program( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, @@ -78,7 +78,7 @@ def create_program( """Upload a new program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). name: Name of the program. description: Program description. max_execution_time: Maximum execution time. @@ -93,6 +93,7 @@ def create_program( """ url = self.get_url('programs') data = {'name': name, + 'data': program_data, 'cost': str(max_execution_time), 'description': description.encode(), 'max_execution_time': max_execution_time, @@ -105,9 +106,7 @@ def create_program( data['returnValues'] = json.dumps(return_values) if interim_results: data['interimResults'] = json.dumps(interim_results) - - files = {'program': (name, program_data)} # type: ignore[dict-item] - response = self.session.post(url, data=data, files=files).json() + response = self.session.post(url, data=data).json() return response def program_run( diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index cc552493e..ab21ec87e 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -12,6 +12,7 @@ """Qiskit runtime service.""" +import base64 import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json @@ -338,7 +339,8 @@ def upload_program( data = file.read() try: - response = self._api_client.program_create(program_data=data.encode(), + program_data = base64.b64encode(data.encode('utf-8')).decode('utf-8') + response = self._api_client.program_create(program_data=program_data, **program_metadata) except RequestsApiError as ex: if ex.status_code == 409: diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index f5990abed..65b433987 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -241,9 +241,6 @@ def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Create a program.""" - if isinstance(program_data, str): - with open(program_data, 'rb') as file: - program_data = file.read() program_id = name if program_id in self._programs: raise RequestsApiError("Program already exists.", status_code=409) From eda2a5112020121d15798cb91488b3e588aa2fab Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 22:41:26 -0400 Subject: [PATCH 12/48] Read programs from "programs" array in response (#161) --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 2 +- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index d13136efb..4aae5de02 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -13,7 +13,7 @@ """Client for accessing IBM Quantum runtime service.""" import logging -from typing import List, Dict, Union, Optional +from typing import Any, List, Dict, Union, Optional from qiskit_ibm.credentials import Credentials from qiskit_ibm.api.session import RetrySession @@ -39,7 +39,7 @@ def __init__( **credentials.connection_parameters()) self.api = Runtime(self._session) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index e04cacbda..8ae956dc2 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -54,7 +54,7 @@ def program_job(self, job_id: str) -> 'ProgramJob': """ return ProgramJob(self.session, job_id) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index ab21ec87e..185df03dd 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -140,7 +140,7 @@ def programs(self, refresh: bool = False) -> List[RuntimeProgram]: if not self._programs or refresh: self._programs = {} response = self._api_client.list_programs() - for prog_dict in response: + for prog_dict in response.get("programs", []): program = self._to_program(prog_dict) self._programs[program.program_id] = program return list(self._programs.values()) diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 65b433987..3a05e45f9 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -235,7 +235,7 @@ def list_programs(self): programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) - return programs + return {"programs": programs} def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, From ab33b5239d2fec9226b013cf2e9fe9b8afe8dcb9 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 20 Oct 2021 23:26:53 -0400 Subject: [PATCH 13/48] Pass program as base64 string to update (#168) --- qiskit_ibm/api/clients/runtime.py | 2 +- qiskit_ibm/api/rest/runtime.py | 4 ++-- qiskit_ibm/runtime/ibm_runtime_service.py | 17 +++++++++++++---- qiskit_ibm/runtime/utils.py | 12 ++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 4aae5de02..ddc71bff9 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -158,7 +158,7 @@ def program_update(self, program_id: str, program_data: str) -> None: Args: program_id: Program ID. - program_data: Program data. + program_data: Program data (base64 encoded). """ self.api.program(program_id).update(program_data) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 8ae956dc2..d5ceff21e 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -238,11 +238,11 @@ def update(self, program_data: str) -> None: """Update a program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). """ url = self.get_url("data") self.session.put(url, data=program_data, - headers={'Content-Type': 'text/plain'}) + headers={'Content-Type': 'application/octet-stream'}) class ProgramJob(RestAdapterBase): diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 185df03dd..cd6a45897 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -12,7 +12,6 @@ """Qiskit runtime service.""" -import base64 import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json @@ -24,7 +23,7 @@ from .runtime_job import RuntimeJob from .runtime_program import RuntimeProgram, ProgramParameter, ProgramResult, ParameterNamespace -from .utils import RuntimeEncoder, RuntimeDecoder +from .utils import RuntimeEncoder, RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) from .program.result_decoder import ResultDecoder @@ -339,7 +338,7 @@ def upload_program( data = file.read() try: - program_data = base64.b64encode(data.encode('utf-8')).decode('utf-8') + program_data = to_base64_string(data) response = self._api_client.program_create(program_data=program_data, **program_metadata) except RequestsApiError as ex: @@ -412,12 +411,22 @@ def update_program( Args: program_id: Program ID. data: Program data or path of the file containing program data to upload. + + Raises: + RuntimeProgramNotFound: If the program doesn't exist. + QiskitRuntimeError: If the request failed. """ if "def main(" not in data: # This is the program file with open(data, "r") as file: data = file.read() - self._api_client.program_update(program_id, data) + try: + program_data = to_base64_string(data) + self._api_client.program_update(program_id, program_data) + except RequestsApiError as ex: + if ex.status_code == 404: + raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise QiskitRuntimeError(f"Failed to update program: {ex}") from None def delete_program(self, program_id: str) -> None: """Delete a runtime program. diff --git a/qiskit_ibm/runtime/utils.py b/qiskit_ibm/runtime/utils.py index 5b46f00ca..b0e4c2a57 100644 --- a/qiskit_ibm/runtime/utils.py +++ b/qiskit_ibm/runtime/utils.py @@ -40,6 +40,18 @@ from qiskit.result import Result +def to_base64_string(data: str) -> str: + """Convert string to base64 string. + + Args: + data: string to convert + + Returns: + data as base64 string + """ + return base64.b64encode(data.encode('utf-8')).decode('utf-8') + + def _serialize_and_encode( data: Any, serializer: Callable, From a8fe605f7ed711a61abd331660dcbcf5ba8b88ca Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 25 Oct 2021 19:16:16 -0400 Subject: [PATCH 14/48] Accept JSON schema as program metadata (#158) * Accept JSON schema as program metadata * Update qiskit_ibm/runtime/ibm_runtime_service.py Co-authored-by: Jessie Yu * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Jessie Yu Co-authored-by: Jessie Yu --- qiskit_ibm/api/clients/runtime.py | 14 +- qiskit_ibm/api/rest/runtime.py | 20 +-- qiskit_ibm/runtime/ibm_runtime_service.py | 109 ++++++--------- .../program/program_metadata_sample.json | 45 +++++-- qiskit_ibm/runtime/runtime_program.py | 124 ++++++++---------- ...metadata-json-schema-46f034ada7443cf9.yaml | 10 ++ test/ibm/runtime/fake_runtime_client.py | 18 ++- test/ibm/runtime/test_runtime.py | 85 +++++++----- 8 files changed, 208 insertions(+), 217 deletions(-) create mode 100644 releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index ddc71bff9..9844f2b76 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -54,10 +54,7 @@ def program_create( description: str, max_execution_time: int, is_public: Optional[bool] = False, - backend_requirements: Optional[Dict] = None, - parameters: Optional[Dict] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None + spec: Optional[Dict] = None ) -> Dict: """Create a new program. @@ -67,10 +64,7 @@ def program_create( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - backend_requirements: Backend requirements. - parameters: Program parameters. - return_values: Program return values. - interim_results: Program interim results. + spec: Backend requirements, parameters, interim results, return values, etc. Returns: Server response. @@ -79,9 +73,7 @@ def program_create( program_data=program_data, name=name, description=description, max_execution_time=max_execution_time, - is_public=is_public, backend_requirements=backend_requirements, - parameters=parameters, return_values=return_values, - interim_results=interim_results + is_public=is_public, spec=spec ) def program_get(self, program_id: str) -> Dict: diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index d5ceff21e..01626efee 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -70,10 +70,7 @@ def create_program( description: str, max_execution_time: int, is_public: Optional[bool] = False, - backend_requirements: Optional[Dict] = None, - parameters: Optional[Dict] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None + spec: Optional[Dict] = None ) -> Dict: """Upload a new program. @@ -83,10 +80,7 @@ def create_program( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - backend_requirements: Backend requirements. - parameters: Program parameters. - return_values: Program return values. - interim_results: Program interim results. + spec: Backend requirements, parameters, interim results, return values, etc. Returns: JSON response. @@ -98,14 +92,8 @@ def create_program( 'description': description.encode(), 'max_execution_time': max_execution_time, 'is_public': is_public} - if backend_requirements: - data['backendRequirements'] = json.dumps(backend_requirements) - if parameters: - data['parameters'] = json.dumps({"doc": parameters}) - if return_values: - data['returnValues'] = json.dumps(return_values) - if interim_results: - data['interimResults'] = json.dumps(interim_results) + if spec is not None: + data['spec'] = json.dumps(spec) response = self.session.post(url, data=data).json() return response diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index cd6a45897..a680afd49 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -15,14 +15,13 @@ import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json -import copy import re from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit_ibm import ibm_provider # pylint: disable=unused-import from .runtime_job import RuntimeJob -from .runtime_program import RuntimeProgram, ProgramParameter, ProgramResult, ParameterNamespace +from .runtime_program import RuntimeProgram, ParameterNamespace from .utils import RuntimeEncoder, RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) @@ -182,21 +181,26 @@ def _to_program(self, response: Dict) -> RuntimeProgram: Returns: A ``RuntimeProgram`` instance. """ - backend_req = json.loads(response.get('backendRequirements', '{}')) - params = json.loads(response.get('parameters', '{}')).get("doc", []) - ret_vals = json.loads(response.get('returnValues', '{}')) - interim_results = json.loads(response.get('interimResults', '{}')) + backend_requirements = {} + parameters = {} + return_values = {} + interim_results = {} + if "spec" in response: + backend_requirements = response["spec"].get('backend_requirements', {}) + parameters = response["spec"].get('parameters', {}) + return_values = response["spec"].get('return_values', {}) + interim_results = response["spec"].get('interim_results', {}) return RuntimeProgram(program_name=response['name'], program_id=response['id'], description=response.get('description', ""), - parameters=params, - return_values=ret_vals, + parameters=parameters, + return_values=return_values, interim_results=interim_results, max_execution_time=response.get('cost', 0), creation_date=response.get('creation_date', ""), update_date=response.get('update_date', ""), - backend_requirements=backend_req, + backend_requirements=backend_requirements, is_public=response.get('is_public', False)) def run( @@ -267,15 +271,7 @@ def run( def upload_program( self, data: str, - metadata: Optional[Union[Dict, str]] = None, - name: Optional[str] = None, - is_public: Optional[bool] = False, - max_execution_time: Optional[int] = None, - description: Optional[str] = None, - backend_requirements: Optional[str] = None, - parameters: Optional[List[ProgramParameter]] = None, - return_values: Optional[List[ProgramResult]] = None, - interim_results: Optional[List[ProgramResult]] = None + metadata: Optional[Union[Dict, str]] = None ) -> str: """Upload a runtime program. @@ -298,17 +294,23 @@ def upload_program( Args: data: Program data or path of the file containing program data to upload. metadata: Name of the program metadata file or metadata dictionary. - A metadata file needs to be in the JSON format. - See :file:`program/program_metadata_sample.yaml` for an example. - name: Name of the program. Required if not specified via `metadata`. - max_execution_time: Maximum execution time in seconds. Required if - not specified via `metadata`. - is_public: Whether the runtime program should be visible to the public. - description: Program description. Required if not specified via `metadata`. - backend_requirements: Backend requirements. - parameters: A list of program input parameters. - return_values: A list of program return values. - interim_results: A list of program interim results. + A metadata file needs to be in the JSON format. The ``parameters``, + ``return_values``, and ``interim_results`` should be defined as JSON Schema. + See :file:`program/program_metadata_sample.json` for an example. The + fields in metadata are explained below. + + * name: Name of the program. Required. + * max_execution_time: Maximum execution time in seconds. Required. + * description: Program description. Required. + * is_public: Whether the runtime program should be visible to the public. + The default is ``False``. + * spec: Specifications for backend characteristics and input parameters + required to run the program, interim results and final result. + + * backend_requirements: Backend requirements. + * parameters: Program input parameters in JSON schema format. + * return_values: Program return values in JSON schema format. + * interim_results: Program interim results in JSON schema format. Returns: Program ID. @@ -319,14 +321,7 @@ def upload_program( IBMNotAuthorizedError: If you are not authorized to upload programs. QiskitRuntimeError: If the upload failed. """ - program_metadata = self._merge_metadata( - initial={}, - metadata=metadata, - name=name, max_execution_time=max_execution_time, - is_public=is_public, description=description, - backend_requirements=backend_requirements, - parameters=parameters, - return_values=return_values, interim_results=interim_results) + program_metadata = self._read_metadata(metadata=metadata) for req in ['name', 'description', 'max_execution_time']: if req not in program_metadata or not program_metadata[req]: @@ -351,21 +346,17 @@ def upload_program( raise QiskitRuntimeError(f"Failed to create program: {ex}") from None return response['id'] - def _merge_metadata( + def _read_metadata( self, - initial: Dict, - metadata: Optional[Union[Dict, str]] = None, - **kwargs: Any + metadata: Optional[Union[Dict, str]] = None ) -> Dict: - """Merge multiple copies of metadata. + """Read metadata. Args: - initial: The initial metadata. This may be mutated. metadata: Name of the program metadata file or metadata dictionary. - **kwargs: Additional metadata fields to overwrite. Returns: - Merged metadata. + Return metadata. """ upd_metadata: dict = {} if metadata is not None: @@ -373,33 +364,11 @@ def _merge_metadata( with open(metadata, 'r') as file: upd_metadata = json.load(file) else: - upd_metadata = copy.deepcopy(metadata) - - self._tuple_to_dict(initial) - initial.update(upd_metadata) - - self._tuple_to_dict(kwargs) - for key, val in kwargs.items(): - if val is not None: - initial[key] = val - + upd_metadata = metadata # TODO validate metadata format metadata_keys = ['name', 'max_execution_time', 'description', - 'backend_requirements', 'parameters', 'return_values', - 'interim_results', 'is_public'] - return {key: val for key, val in initial.items() if key in metadata_keys} - - def _tuple_to_dict(self, metadata: Dict) -> None: - """Convert fields in metadata from named tuples to dictionaries. - - Args: - metadata: Metadata to be converted. - """ - for key in ['parameters', 'return_values', 'interim_results']: - doc_list = metadata.pop(key, None) - if not doc_list or isinstance(doc_list[0], dict): - continue - metadata[key] = [dict(elem._asdict()) for elem in doc_list] + 'spec', 'is_public'] + return {key: val for key, val in upd_metadata.items() if key in metadata_keys} def update_program( self, diff --git a/qiskit_ibm/runtime/program/program_metadata_sample.json b/qiskit_ibm/runtime/program/program_metadata_sample.json index 5df24385f..6449b474b 100644 --- a/qiskit_ibm/runtime/program/program_metadata_sample.json +++ b/qiskit_ibm/runtime/program/program_metadata_sample.json @@ -2,15 +2,38 @@ "name": "runtime-simple", "description": "Simple runtime program used for testing.", "max_execution_time": 300, - "backend_requirements": {"min_num_qubits": 5}, - "parameters": [ - {"name": "iterations", "description": "Number of iterations to run. Each iteration generates a runs a random circuit.", "type": "integer", "required": true} - ], - "return_values": [ - {"name": "-", "description": "A string that says 'All done!'.", "type": "string"} - ], - "interim_results": [ - {"name": "iteration", "description": "Iteration number.", "type": "int"}, - {"name": "counts", "description": "Histogram data of the circuit result.", "type": "dict"} - ] + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "type": "object", + "properties": { + "iterations": { + "description": "Number of iterations to run. Each iteration generates and runs a random circuit.", + "type": "integer" + } + }, + "required": [ + "iterations" + ] + }, + "return_values": { + "type": "string", + "description": "A string that says 'All done!'." + }, + "interim_results": { + "type": "object", + "properties": { + "iteration": { + "description": "Iteration number.", + "type": "integer" + }, + "counts": { + "description": "Histogram data of the circuit result.", + "type": "object" + } + } + } + } } \ No newline at end of file diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index 9b6cde746..5e648244e 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -13,7 +13,8 @@ """Qiskit runtime program.""" import logging -from typing import Optional, List, NamedTuple, Dict +import re +from typing import Optional, List, Dict from types import SimpleNamespace from qiskit_ibm.exceptions import IBMInputValueError @@ -47,9 +48,9 @@ def __init__( program_name: str, program_id: str, description: str, - parameters: Optional[List] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None, + parameters: Optional[Dict] = None, + return_values: Optional[Dict] = None, + interim_results: Optional[Dict] = None, max_execution_time: int = 0, backend_requirements: Optional[Dict] = None, creation_date: str = "", @@ -76,49 +77,44 @@ def __init__( self._description = description self._max_execution_time = max_execution_time self._backend_requirements = backend_requirements or {} - self._parameters: List[ProgramParameter] = [] - self._return_values: List[ProgramResult] = [] - self._interim_results: List[ProgramResult] = [] + self._parameters = parameters or {} + self._return_values = return_values or {} + self._interim_results = interim_results or {} self._creation_date = creation_date self._update_date = update_date self._is_public = is_public - if parameters: - for param in parameters: - self._parameters.append( - ProgramParameter(name=param['name'], - description=param['description'], - type=param['type'], - required=param['required'])) - if return_values is not None: - for ret in return_values: - self._return_values.append(ProgramResult(name=ret['name'], - description=ret['description'], - type=ret['type'])) - if interim_results is not None: - for intret in interim_results: - self._interim_results.append(ProgramResult(name=intret['name'], - description=intret['description'], - type=intret['type'])) - def __str__(self) -> str: - def _format_common(items: List) -> None: - """Add name, description, and type to `formatted`.""" - for item in items: - formatted.append(" "*4 + "- " + item.name + ":") - formatted.append(" "*6 + "Description: " + item.description) - formatted.append(" "*6 + "Type: " + item.type) - if hasattr(item, 'required'): - formatted.append(" "*6 + "Required: " + str(item.required)) + def _format_common(schema: Dict) -> None: + """Add title, description and property details to `formatted`.""" + if "description" in schema: + formatted.append(" "*4 + "Description: {}".format(schema["description"])) + if "type" in schema: + formatted.append(" "*4 + "Type: {}".format(str(schema["type"]))) + if "properties" in schema: + formatted.append(" "*4 + "Properties:") + for property_name, property_value in schema["properties"].items(): + formatted.append(" "*8 + "- " + property_name + ":") + for key, value in property_value.items(): + formatted.append(" "*12 + "{}: {}".format(sentence_case(key), str(value))) + formatted.append(" "*12 + "Required: " + + str(property_name in schema.get("required", []))) + + def sentence_case(camel_case_text: str) -> str: + """Converts camelCase to Sentence case""" + if camel_case_text == '': + return camel_case_text + sentence_case_text = re.sub('([A-Z])', r' \1', camel_case_text) + return sentence_case_text[:1].upper() + sentence_case_text[1:].lower() formatted = [f'{self.program_id}:', f" Name: {self.name}", f" Description: {self.description}", f" Creation date: {self.creation_date}", f" Update date: {self.update_date}", - f" Max execution time: {self.max_execution_time}", - f" Input parameters:"] + f" Max execution time: {self.max_execution_time}"] + formatted.append(" Input parameters:") if self._parameters: _format_common(self._parameters) else: @@ -198,7 +194,7 @@ def description(self) -> str: return self._description @property - def return_values(self) -> List['ProgramResult']: + def return_values(self) -> Dict: """Program return value definitions. Returns: @@ -207,7 +203,7 @@ def return_values(self) -> List['ProgramResult']: return self._return_values @property - def interim_results(self) -> List['ProgramResult']: + def interim_results(self) -> Dict: """Program interim result definitions. Returns: @@ -263,21 +259,6 @@ def is_public(self) -> bool: return self._is_public -class ProgramParameter(NamedTuple): - """Program parameter.""" - name: str - description: str - type: str - required: bool - - -class ProgramResult(NamedTuple): - """Program result.""" - name: str - description: str - type: str - - class ParameterNamespace(SimpleNamespace): """ A namespace for program parameters with validation. @@ -285,26 +266,26 @@ class ParameterNamespace(SimpleNamespace): and validation support. """ - def __init__(self, params: List[ProgramParameter]): + def __init__(self, parameters: Dict): """ParameterNamespace constructor. Args: - params: The program's input parameters. + parameters: The program's input parameters. """ super().__init__() - # Allow access to the raw program parameters list - self.__metadata = params + # Allow access to the raw program parameters dict + self.__metadata = parameters # For localized logic, create store of parameters in dictionary self.__program_params: dict = {} - for param in params: + for parameter_name, parameter_value in parameters.get("properties", {}).items(): # (1) Add parameters to a dict by name - setattr(self, param.name, None) + setattr(self, parameter_name, None) # (2) Store the program params for validation - self.__program_params[param.name] = param + self.__program_params[parameter_name] = parameter_value @property - def metadata(self) -> List[ProgramParameter]: + def metadata(self) -> Dict: """Returns the parameter metadata""" return self.__metadata @@ -320,12 +301,12 @@ def validate(self) -> None: """ # Iterate through the user's stored inputs - for param_name, program_param in self.__program_params.items(): - # Set invariants: User-specified parameter value (value) and whether it's required (req) - value = getattr(self, param_name, None) + for parameter_name, parameter_value in self.__program_params.items(): + # Set invariants: User-specified parameter value (value) and if it's required (req) + value = getattr(self, parameter_name, None) # Check there exists a program parameter of that name. - if value is None and program_param.required: - raise IBMInputValueError('Param (%s) missing required value!' % param_name) + if value is None and parameter_name in self.metadata.get("required", []): + raise IBMInputValueError('Param (%s) missing required value!' % parameter_name) def __str__(self) -> str: """Creates string representation of object""" @@ -338,15 +319,14 @@ def __str__(self) -> str: 'Required', 'Description' ) - # List of ProgramParameter objects (str) params_str = '\n'.join([ '| {:10.10} | {:12.12} | {:12.12}| {:8.8} | {:>15} |'.format( - param.name, - str(getattr(self, param.name, 'None')), - param.type, - str(param.required), - param.description - ) for param in self.__program_params.values()]) + parameter_name, + str(getattr(self, parameter_name, "None")), + str(parameter_value.get("type", "None")), + str(parameter_name in self.metadata.get("required", [])), + str(parameter_value.get("description", "None")) + ) for parameter_name, parameter_value in self.__program_params.items()]) return "ParameterNamespace (Values):\n%s\n%s\n%s" \ % (header, '-' * len(header), params_str) diff --git a/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml b/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml new file mode 100644 index 000000000..08eb17d93 --- /dev/null +++ b/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + :meth:`qiskit_ibm.runtime.IBMRuntimeService.upload_program` now takes only two parameters, + ``data``, which is the program passed as a string or the path to the program file and the + ``metadata``, which is passed as a dictionary or path to the metadata JSON file. + In ``metadata`` the ``backend_requirements``, ``parameters``, ``return_values`` and + ``interim_results`` are now grouped under a specifications ``spec`` section. + ``parameters``, ``return_values`` and ``interim_results`` should now be specified as + JSON Schema. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 3a05e45f9..bd2b4852d 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -52,14 +52,15 @@ def to_dict(self, include_data=False): 'update_date': '2021-09-14T19:25:32Z'} if include_data: out['data'] = self._data + out['spec'] = {} if self._backend_requirements: - out['backendRequirements'] = json.dumps(self._backend_requirements) + out['spec']['backend_requirements'] = self._backend_requirements if self._parameters: - out['parameters'] = json.dumps({"doc": self._parameters}) + out['spec']['parameters'] = self._parameters if self._return_values: - out['returnValues'] = json.dumps(self._return_values) + out['spec']['return_values'] = self._return_values if self._interim_results: - out['interimResults'] = json.dumps(self._interim_results) + out['spec']['interim_results'] = self._interim_results return out @@ -231,19 +232,22 @@ def set_final_status(self, final_status): self._final_status = final_status def list_programs(self): - """List all progrmas.""" + """List all programs.""" programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) return {"programs": programs} def program_create(self, program_data, name, description, max_execution_time, - backend_requirements=None, parameters=None, return_values=None, - interim_results=None, is_public=False): + spec=None, is_public=False): """Create a program.""" program_id = name if program_id in self._programs: raise RequestsApiError("Program already exists.", status_code=409) + backend_requirements = spec.get('backend_requirements', None) + parameters = spec.get('parameters', None) + return_values = spec.get('return_values', None) + interim_results = spec.get('interim_results', None) self._programs[program_id] = BaseFakeProgram( program_id=program_id, name=name, data=program_data, cost=max_execution_time, description=description, backend_requirements=backend_requirements, diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index acedbea2f..954755b43 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -12,6 +12,7 @@ """Tests for runtime service.""" +import copy import json import os from io import StringIO @@ -55,7 +56,7 @@ from qiskit_ibm.runtime import IBMRuntimeService, RuntimeJob from qiskit_ibm.runtime.constants import API_TO_JOB_ERROR_MESSAGE from qiskit_ibm.runtime.exceptions import RuntimeProgramNotFound, RuntimeJobFailureError -from qiskit_ibm.runtime.runtime_program import ParameterNamespace, ProgramParameter, ProgramResult +from qiskit_ibm.runtime.runtime_program import ParameterNamespace from ...ibm_test_case import IBMTestCase from .fake_runtime_client import (BaseFakeRuntimeClient, FailedRanTooLongRuntimeJob, @@ -70,16 +71,50 @@ class TestRuntime(IBMTestCase): "name": "qiskit-test", "description": "Test program.", "max_execution_time": 300, - "backend_requirements": {"min_num_qubits": 5}, - "parameters": [ - {'name': 'param1', 'description': 'Desc 1', 'type': 'str', 'required': True}, - {'name': 'param2', 'description': 'Desc 2', 'type': 'int', 'required': False}], - "return_values": [ - {"name": "ret_val", "description": "Some return value.", "type": "string"} - ], - "interim_results": [ - {"name": "int_res", "description": "Some interim result", "type": "string"}, - ] + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "properties": { + "param1": { + "description": "Desc 1", + "type": "string", + "enum": [ + "a", + "b", + "c" + ] + }, + "param2": { + "description": "Desc 2", + "type": "integer", + "min": 0 + } + }, + "required": [ + "param1" + ] + }, + "return_values": { + "type": "object", + "description": "Return values", + "properties": { + "ret_val": { + "description": "Some return value.", + "type": "string" + } + } + }, + "interim_results": { + "properties": { + "int_res": { + "description": "Some interim result", + "type": "string" + } + } + } + } } def setUp(self): @@ -601,26 +636,15 @@ def test_program_metadata(self): program.max_execution_time) self.assertTrue(program.creation_date) self.assertTrue(program.update_date) - self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], + self.assertEqual(self.DEFAULT_METADATA['spec']['backend_requirements'], program.backend_requirements) - self.assertEqual([ProgramParameter(**param) for param in - self.DEFAULT_METADATA['parameters']], + self.assertEqual(self.DEFAULT_METADATA['spec']['parameters'], program.parameters().metadata) - self.assertEqual([ProgramResult(**ret) for ret in - self.DEFAULT_METADATA['return_values']], + self.assertEqual(self.DEFAULT_METADATA['spec']['return_values'], program.return_values) - self.assertEqual([ProgramResult(**ret) for ret in - self.DEFAULT_METADATA['interim_results']], + self.assertEqual(self.DEFAULT_METADATA['spec']['interim_results'], program.interim_results) - def test_metadata_combined(self): - """Test combining metadata""" - update_metadata = {"max_execution_time": 600} - program_id = self.runtime.upload_program( - data="def main() {}", metadata=self.DEFAULT_METADATA, **update_metadata) - program = self.runtime.program(program_id) - self.assertEqual(update_metadata['max_execution_time'], program.max_execution_time) - def test_different_providers(self): """Test retrieving job submitted with different provider.""" program_id = self._upload_program() @@ -636,12 +660,13 @@ def _upload_program(self, name=None, max_execution_time=300, """Upload a new program.""" name = name or uuid.uuid4().hex data = "def main() {}" + metadata = copy.deepcopy(self.DEFAULT_METADATA) + metadata.update(name=name) + metadata.update(is_public=is_public) + metadata.update(max_execution_time=max_execution_time) program_id = self.runtime.upload_program( - name=name, data=data, - is_public=is_public, - max_execution_time=max_execution_time, - description="A test program") + metadata=metadata) return program_id def _run_program(self, program_id=None, inputs=None, job_classes=None, final_status=None, From df4eb817b0dc8e5e8b15ebc0918bfe8fdfa5428d Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 27 Oct 2021 15:43:37 -0400 Subject: [PATCH 15/48] Pass program params as object (#171) --- qiskit_ibm/api/clients/runtime.py | 2 +- qiskit_ibm/api/rest/runtime.py | 7 ++++--- qiskit_ibm/runtime/ibm_runtime_service.py | 5 ++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 9844f2b76..01ff6aff2 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -117,7 +117,7 @@ def program_run( program_id: str, credentials: Credentials, backend_name: str, - params: str, + params: Dict, image: str ) -> Dict: """Run the specified program. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 01626efee..f168ebf4a 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -19,6 +19,7 @@ from .base import RestAdapterBase from ..session import RetrySession +from ...runtime.utils import RuntimeEncoder logger = logging.getLogger(__name__) @@ -104,7 +105,7 @@ def program_run( group: str, project: str, backend_name: str, - params: str, + params: Dict, image: str ) -> Dict: """Execute the program. @@ -128,10 +129,10 @@ def program_run( 'group': group, 'project': project, 'backend': backend_name, - 'params': [params], + 'params': params, 'runtime': image } - data = json.dumps(payload) + data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> Dict: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index a680afd49..374fd8020 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -22,7 +22,7 @@ from .runtime_job import RuntimeJob from .runtime_program import RuntimeProgram, ParameterNamespace -from .utils import RuntimeEncoder, RuntimeDecoder, to_base64_string +from .utils import RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) from .program.result_decoder import ResultDecoder @@ -250,12 +250,11 @@ def run( raise IBMInputValueError('"image" needs to be in form of image_name:tag') backend_name = options['backend_name'] - params_str = json.dumps(inputs, cls=RuntimeEncoder) result_decoder = result_decoder or ResultDecoder response = self._api_client.program_run(program_id=program_id, credentials=self._provider.credentials, backend_name=backend_name, - params=params_str, + params=inputs, image=image) backend = self._provider.get_backend(backend_name) From e3e431561648d390f44d21c26d870bc50d27ebb6 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 27 Oct 2021 14:50:50 -0400 Subject: [PATCH 16/48] Fix integration tests --- qiskit_ibm/api/rest/runtime.py | 17 +++++++------- qiskit_ibm/runtime/ibm_runtime_service.py | 5 ++-- test/ibm/runtime/test_runtime_integration.py | 24 ++++++++------------ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index f168ebf4a..7e774b978 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -87,16 +87,15 @@ def create_program( JSON response. """ url = self.get_url('programs') - data = {'name': name, - 'data': program_data, - 'cost': str(max_execution_time), - 'description': description.encode(), - 'max_execution_time': max_execution_time, - 'is_public': is_public} + payload = {'name': name, + 'data': program_data, + 'cost': max_execution_time, + 'description': description, + 'is_public': is_public} if spec is not None: - data['spec'] = json.dumps(spec) - response = self.session.post(url, data=data).json() - return response + payload['spec'] = spec + data = json.dumps(payload) + return self.session.post(url, data=data).json() def program_run( self, diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 374fd8020..7967fe039 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -120,8 +120,9 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None if detailed: print(str(prog)) else: - print(f"Name: {prog.name}") - print(f"Description: {prog.description}") + print(f"{prog.program_id}:",) + print(f" Name: {prog.name}") + print(f" Description: {prog.description}") def programs(self, refresh: bool = False) -> List[RuntimeProgram]: """Return available runtime programs. diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 34869db2a..101440c19 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -12,6 +12,7 @@ """Tests for runtime service.""" +import copy import unittest import os import uuid @@ -85,11 +86,12 @@ def setUpClass(cls, backend): cls.backend = backend cls.poll_time = 1 if backend.configuration().simulator else 5 cls.provider = backend.provider() + metadata = copy.deepcopy(cls.RUNTIME_PROGRAM_METADATA) + metadata['name'] = cls._get_program_name() try: cls.program_id = cls.provider.runtime.upload_program( - name=cls._get_program_name(), data=cls.RUNTIME_PROGRAM, - metadata=cls.RUNTIME_PROGRAM_METADATA) + metadata=metadata) except RuntimeDuplicateProgramError: pass except IBMNotAuthorizedError: @@ -200,13 +202,6 @@ def test_set_visibility(self): # Verify changed self.assertNotEqual(start_vis, end_vis) - def test_upload_program_conflict(self): - """Test uploading a program with conflicting name.""" - name = self._get_program_name() - self._upload_program(name=name) - with self.assertRaises(RuntimeDuplicateProgramError): - self._upload_program(name=name) - def test_delete_program(self): """Test deleting program.""" program_id = self._upload_program() @@ -626,6 +621,7 @@ def _validate_program(self, program): self.assertTrue(program.description) self.assertTrue(program.max_execution_time) self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) def _upload_program( self, @@ -636,13 +632,13 @@ def _upload_program( """Upload a new program.""" name = name or self._get_program_name() data = data or self.RUNTIME_PROGRAM + metadata = copy.deepcopy(self.RUNTIME_PROGRAM_METADATA) + metadata['name'] = name + metadata['max_execution_time'] = max_execution_time + metadata['is_public'] = is_public program_id = self.provider.runtime.upload_program( - name=name, data=data, - is_public=is_public, - metadata=self.RUNTIME_PROGRAM_METADATA, - max_execution_time=max_execution_time, - description="Qiskit test program") + metadata=metadata) self.to_delete.append(program_id) return program_id From 0f958251c7523d66b2952beb381823d649b1babd Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Thu, 28 Oct 2021 14:28:21 -0400 Subject: [PATCH 17/48] Use count to reduce one last extra call to API (#172) --- qiskit_ibm/runtime/ibm_runtime_service.py | 19 +++++++++++++------ test/ibm/runtime/fake_runtime_client.py | 4 +++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 7967fe039..55718335c 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -477,17 +477,24 @@ def jobs( """ job_responses = [] # type: List[Dict[str, Any]] current_page_limit = limit or 20 + offset = skip while True: - job_page = self._api_client.jobs_get( + jobs_response = self._api_client.jobs_get( limit=current_page_limit, - skip=skip, - pending=pending)["jobs"] - if not job_page: + skip=offset, + pending=pending) + job_page = jobs_response["jobs"] + # count is the total number of jobs that would be returned if + # there was no limit or skip + count = jobs_response["count"] + + job_responses += job_page + + if len(job_responses) == count - skip: # Stop if there are no more jobs returned by the server. break - job_responses += job_page if limit: if len(job_responses) >= limit: # Stop if we have reached the limit. @@ -496,7 +503,7 @@ def jobs( else: current_page_limit = 20 - skip += len(job_page) + offset += len(job_page) return [self._decode_job(job) for job in job_responses] diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index bd2b4852d..bdf4fa0d7 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -301,12 +301,14 @@ def jobs_get(self, limit=None, skip=None, pending=None): limit = limit or len(self._jobs) skip = skip or 0 jobs = list(self._jobs.values()) + count = len(self._jobs) if pending is not None: job_status_list = pending_statuses if pending else returned_statuses jobs = [job for job in jobs if job._status in job_status_list] + count = len(jobs) jobs = jobs[skip:limit+skip] return {"jobs": [job.to_dict() for job in jobs], - "count": len(self._jobs)} + "count": count} def set_program_visibility(self, program_id: str, public: bool) -> None: """Sets a program's visibility. From b532b1476046eb7faa31f6027ae2f789675a804b Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Fri, 29 Oct 2021 13:56:22 -0400 Subject: [PATCH 18/48] Allow updating runtime metadata in place (#188) * update runtime metadata * return if no data * fix mypy * Update releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml Co-authored-by: Rathish Cholarajan --- qiskit_ibm/api/clients/runtime.py | 33 +++++---- qiskit_ibm/api/rest/runtime.py | 41 ++++++++--- qiskit_ibm/runtime/ibm_runtime_service.py | 68 +++++++++++++++++-- ...ate-runtime-metadata-d2ddbcfc0d034530.yaml | 9 +++ test/ibm/runtime/fake_runtime_client.py | 35 ++++++++-- test/ibm/runtime/test_runtime.py | 47 +++++++++++++ test/ibm/runtime/test_runtime_integration.py | 27 +++++++- 7 files changed, 220 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 01ff6aff2..330893c2d 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -87,17 +87,6 @@ def program_get(self, program_id: str) -> Dict: """ return self.api.program(program_id).get() - def program_get_data(self, program_id: str) -> Dict: - """Return a specific program and its data. - - Args: - program_id: Program ID. - - Returns: - Program information, including data. - """ - return self.api.program(program_id).get_data() - def set_program_visibility(self, program_id: str, public: bool) -> None: """Sets a program's visibility. @@ -145,14 +134,32 @@ def program_delete(self, program_id: str) -> None: """ self.api.program(program_id).delete() - def program_update(self, program_id: str, program_data: str) -> None: + def program_update( + self, + program_id: str, + program_data: str = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None + ) -> None: """Update a program. Args: program_id: Program ID. program_data: Program data (base64 encoded). + name: Name of the program. + description: Program description. + max_execution_time: Maximum execution time. + spec: Backend requirements, parameters, interim results, return values, etc. """ - self.api.program(program_id).update(program_data) + if program_data: + self.api.program(program_id).update_data(program_data) + + if any([name, description, max_execution_time, spec]): + self.api.program(program_id).update_metadata( + name=name, description=description, + max_execution_time=max_execution_time, spec=spec) def job_get(self, job_id: str) -> Dict: """Get job data. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 7e774b978..d5aacd673 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -194,15 +194,6 @@ def get(self) -> Dict[str, Any]: url = self.get_url('self') return self.session.get(url).json() - def get_data(self) -> Dict[str, Any]: - """Return program information, including data. - - Returns: - JSON response. - """ - url = self.get_url('data') - return self.session.get(url).json() - def make_public(self) -> None: """Sets a runtime program's visibility to public.""" url = self.get_url('public') @@ -222,8 +213,8 @@ def delete(self) -> None: url = self.get_url('self') self.session.delete(url) - def update(self, program_data: str) -> None: - """Update a program. + def update_data(self, program_data: str) -> None: + """Update program data. Args: program_data: Program data (base64 encoded). @@ -232,6 +223,34 @@ def update(self, program_data: str) -> None: self.session.put(url, data=program_data, headers={'Content-Type': 'application/octet-stream'}) + def update_metadata( + self, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None + ) -> None: + """Update program metadata. + + Args: + name: Name of the program. + description: Program description. + max_execution_time: Maximum execution time. + spec: Backend requirements, parameters, interim results, return values, etc. + """ + url = self.get_url("self") + payload: Dict = {} + if name: + payload["name"] = name + if description: + payload["description"] = description + if max_execution_time: + payload["cost"] = max_execution_time + if spec: + payload["spec"] = spec + + self.session.patch(url, json=payload) + class ProgramJob(RestAdapterBase): """Rest adapter for program job related endpoints.""" diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 55718335c..d5ad9081f 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -16,6 +16,7 @@ from typing import Dict, Callable, Optional, Union, List, Any, Type import json import re +import warnings from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit_ibm import ibm_provider # pylint: disable=unused-import @@ -373,30 +374,83 @@ def _read_metadata( def update_program( self, program_id: str, - data: str, + data: str = None, + metadata: Optional[Union[Dict, str]] = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None ) -> None: """Update a runtime program. + Program metadata can be specified using the `metadata` parameter or + individual parameters, such as `name` and `description`. If the + same metadata field is specified in both places, the individual parameter + takes precedence. + Args: program_id: Program ID. data: Program data or path of the file containing program data to upload. + metadata: Name of the program metadata file or metadata dictionary. + name: New program name. + description: New program description. + max_execution_time: New maximum execution time. + spec: New specifications for backend characteristics, input parameters, + interim results and final result. Raises: RuntimeProgramNotFound: If the program doesn't exist. QiskitRuntimeError: If the request failed. """ - if "def main(" not in data: - # This is the program file - with open(data, "r") as file: - data = file.read() + if not any([data, metadata, name, description, max_execution_time, spec]): + warnings.warn("None of the 'data', 'metadata', 'name', 'description', " + "'max_execution_time', or 'spec' parameters is specified. " + "No update is made.") + return + + if data: + if "def main(" not in data: + # This is the program file + with open(data, "r") as file: + data = file.read() + data = to_base64_string(data) + + if metadata: + metadata = self._read_metadata(metadata=metadata) + combined_metadata = self._merge_metadata( + metadata=metadata, name=name, description=description, + max_execution_time=max_execution_time, spec=spec) + try: - program_data = to_base64_string(data) - self._api_client.program_update(program_id, program_data) + self._api_client.program_update( + program_id, program_data=data, **combined_metadata) except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None raise QiskitRuntimeError(f"Failed to update program: {ex}") from None + def _merge_metadata( + self, + metadata: Optional[Dict] = None, + **kwargs: Any + ) -> Dict: + """Merge multiple copies of metadata. + Args: + metadata: Program metadata. + **kwargs: Additional metadata fields to overwrite. + Returns: + Merged metadata. + """ + merged = {} + metadata = metadata or {} + metadata_keys = ['name', 'max_execution_time', 'description', 'spec'] + for key in metadata_keys: + if kwargs.get(key, None) is not None: + merged[key] = kwargs[key] + elif key in metadata.keys(): + merged[key] = metadata[key] + return merged + def delete_program(self, program_id: str) -> None: """Delete a runtime program. diff --git a/releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml b/releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml new file mode 100644 index 000000000..18ff3d661 --- /dev/null +++ b/releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + You can now use the :meth:`qiskit_ibm.runtime.IBMRuntimeService.update_program` + method to update the metadata for a Qiskit Runtime program. + Program metadata can be specified using the ``metadata`` parameter or + individual parameters, such as ``name`` and ``description``. If the + same metadata field is specified in both places, the individual parameter + takes precedence. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index bdf4fa0d7..3e28c7de0 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -15,7 +15,8 @@ import time import uuid import json -from typing import Optional +import base64 +from typing import Optional, Dict from concurrent.futures import ThreadPoolExecutor from qiskit_ibm.credentials import Credentials @@ -51,7 +52,7 @@ def to_dict(self, include_data=False): 'creation_date': '2021-09-13T17:27:42Z', 'update_date': '2021-09-14T19:25:32Z'} if include_data: - out['data'] = self._data + out['data'] = base64.standard_b64decode(self._data).decode() out['spec'] = {} if self._backend_requirements: out['spec']['backend_requirements'] = self._backend_requirements @@ -255,15 +256,35 @@ def program_create(self, program_data, name, description, max_execution_time, is_public=is_public) return {'id': program_id} + def program_update( + self, + program_id: str, + program_data: str = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None + ) -> None: + """Update a program.""" + if program_id not in self._programs: + raise RequestsApiError("Program not found", status_code=404) + program = self._programs[program_id] + program._data = program_data or program._data + program._name = name or program._name + program._description = description or program._description + program._cost = max_execution_time or program._cost + if spec: + program._backend_requirements = \ + spec.get("backend_requirements") or program._backend_requirements + program._parameters = spec.get("parameters") or program._parameters + program._return_values = spec.get("return_values") or program._return_values + program._interim_results = spec.get("interim_results") or program._interim_results + def program_get(self, program_id: str): """Return a specific program.""" if program_id not in self._programs: raise RequestsApiError("Program not found", status_code=404) - return self._programs[program_id].to_dict() - - def program_get_data(self, program_id: str): - """Return a specific program and its data.""" - return self._programs[program_id].to_dict(iclude_data=True) + return self._programs[program_id].to_dict(include_data=True) def program_run( self, diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index 954755b43..c7ae84d20 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -340,6 +340,53 @@ def test_upload_program(self): self.assertEqual(max_execution_time, program.max_execution_time) self.assertEqual(program.is_public, is_public) + def test_update_program(self): + """Test updating program.""" + new_data = "def main() {foo=bar}" + new_metadata = copy.deepcopy(self.DEFAULT_METADATA) + new_metadata["name"] = "test_update_program" + new_name = "name2" + new_description = "some other description" + new_cost = self.DEFAULT_METADATA["max_execution_time"] + 100 + new_spec = copy.deepcopy(self.DEFAULT_METADATA["spec"]) + new_spec["backend_requirements"] = {"input_allowed": "runtime"} + + sub_tests = [ + {"data": new_data}, + {"metadata": new_metadata}, + {"data": new_data, "metadata": new_metadata}, + {"metadata": new_metadata, "name": new_name}, + {"data": new_data, "metadata": new_metadata, "description": new_description}, + {"max_execution_time": new_cost, "spec": new_spec} + ] + + for new_vals in sub_tests: + with self.subTest(new_vals=new_vals.keys()): + program_id = self._upload_program() + self.runtime.update_program(program_id=program_id, **new_vals) + updated = self.runtime.program(program_id, refresh=True) + if "data" in new_vals: + raw_program = self.runtime._api_client.program_get(program_id) + self.assertEqual(new_data, raw_program["data"]) + if "metadata" in new_vals and "name" not in new_vals: + self.assertEqual(new_metadata["name"], updated.name) + if "name" in new_vals: + self.assertEqual(new_name, updated.name) + if "description" in new_vals: + self.assertEqual(new_description, updated.description) + if "max_execution_time" in new_vals: + self.assertEqual(new_cost, updated.max_execution_time) + if "spec" in new_vals: + raw_program = self.runtime._api_client.program_get(program_id) + self.assertEqual(new_spec, raw_program["spec"]) + + def test_update_program_no_new_fields(self): + """Test updating a program without any new data.""" + program_id = self._upload_program() + with warnings.catch_warnings(record=True) as warn_cm: + self.runtime.update_program(program_id=program_id) + self.assertEqual(len(warn_cm), 1) + def test_delete_program(self): """Test deleting program.""" program_id = self._upload_program() diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 101440c19..b0d69c01e 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -216,8 +216,8 @@ def test_double_delete_program(self): with self.assertRaises(RuntimeProgramNotFound): self.provider.runtime.delete_program(program_id) - def test_update_program(self): - """Test updating a program.""" + def test_update_program_data(self): + """Test updating program data.""" program_v1 = """ def main(backend, user_messenger, **kwargs): return "version 1" @@ -226,6 +226,7 @@ def main(backend, user_messenger, **kwargs): def main(backend, user_messenger, **kwargs): return "version 2" """ + # TODO retrieve program data instead of run program when #66 is merged program_id = self._upload_program(data=program_v1) job = self._run_program(program_id=program_id) self.assertEqual("version 1", job.result()) @@ -233,6 +234,28 @@ def main(backend, user_messenger, **kwargs): job = self._run_program(program_id=program_id) self.assertEqual("version 2", job.result()) + def test_update_program_metadata(self): + """Test updating program metadata.""" + program_id = self._upload_program() + original = self.provider.runtime.program(program_id) + new_metadata = { + "name": self._get_program_name(), + "description": "test_update_program_metadata", + "max_execution_time": original.max_execution_time + 100, + "spec": { + "return_values": { + "type": "object", + "description": "Some return value" + } + } + } + self.provider.runtime.update_program(program_id=program_id, metadata=new_metadata) + updated = self.provider.runtime.program(program_id, refresh=True) + self.assertEqual(new_metadata["name"], updated.name) + self.assertEqual(new_metadata["description"], updated.description) + self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) + self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) + def test_run_program(self): """Test running a program.""" job = self._run_program(final_result="foo") From f5b4e547a3c233c25c5fa35b53e480346038f384 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 01:14:35 -0400 Subject: [PATCH 19/48] Remove version field from runtime program (#152) * Remove version field from runtime program * Add release note --- qiskit_ibm/api/clients/runtime.py | 4 +--- qiskit_ibm/api/rest/runtime.py | 4 ---- qiskit_ibm/runtime/__init__.py | 7 ++----- qiskit_ibm/runtime/ibm_runtime_service.py | 7 ++----- .../runtime/program/program_metadata_sample.json | 1 - qiskit_ibm/runtime/runtime_program.py | 14 -------------- .../remove-version-field-0543061d4a7e059a.yaml | 4 ++++ test/ibm/runtime/fake_runtime_client.py | 8 +++----- test/ibm/runtime/test_runtime.py | 9 +++------ test/ibm/runtime/test_runtime_integration.py | 1 - 10 files changed, 15 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 4156c4999..05b8264d0 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -54,7 +54,6 @@ def program_create( description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, backend_requirements: Optional[Dict] = None, parameters: Optional[Dict] = None, return_values: Optional[List] = None, @@ -68,7 +67,6 @@ def program_create( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. backend_requirements: Backend requirements. parameters: Program parameters. return_values: Program return values. @@ -81,7 +79,7 @@ def program_create( program_data=program_data, name=name, description=description, max_execution_time=max_execution_time, - is_public=is_public, version=version, backend_requirements=backend_requirements, + is_public=is_public, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results ) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index e8252ee0b..da8cf3487 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -70,7 +70,6 @@ def create_program( description: str, max_execution_time: int, is_public: Optional[bool] = False, - version: Optional[str] = None, backend_requirements: Optional[Dict] = None, parameters: Optional[Dict] = None, return_values: Optional[List] = None, @@ -84,7 +83,6 @@ def create_program( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - version: Program version. backend_requirements: Backend requirements. parameters: Program parameters. return_values: Program return values. @@ -99,8 +97,6 @@ def create_program( 'description': description.encode(), 'max_execution_time': max_execution_time, 'isPublic': is_public} - if version is not None: - data['version'] = version if backend_requirements: data['backendRequirements'] = json.dumps(backend_requirements) if parameters: diff --git a/qiskit_ibm/runtime/__init__.py b/qiskit_ibm/runtime/__init__.py index 83012f6e2..7b8fc84aa 100644 --- a/qiskit_ibm/runtime/__init__.py +++ b/qiskit_ibm/runtime/__init__.py @@ -191,14 +191,11 @@ def interim_result_callback(job_id, interim_result): provider = IBMProvider() program_id = provider.runtime.upload_program( data="my_vqe.py", - metadata="my_vqe_metadata.json", - version="1.2" + metadata="my_vqe_metadata.json" ) In the example above, the file ``my_vqe.py`` contains the program data, and -``my_vqe_metadata.json`` contains the program metadata. An additional -parameter ``version`` is also specified, which takes precedence over any -``version`` value specified in ``my_vqe_metadata.json``. +``my_vqe_metadata.json`` contains the program metadata. Method :meth:`IBMRuntimeService.delete_program` allows you to delete a program. diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 5d10f1d0e..febe967f6 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -195,7 +195,6 @@ def _to_program(self, response: Dict) -> RuntimeProgram: interim_results=interim_results, max_execution_time=response.get('cost', 0), creation_date=response.get('creationDate', ""), - version=response.get('version', "0"), backend_requirements=backend_req, is_public=response.get('isPublic', False)) @@ -272,7 +271,6 @@ def upload_program( is_public: Optional[bool] = False, max_execution_time: Optional[int] = None, description: Optional[str] = None, - version: Optional[float] = None, backend_requirements: Optional[str] = None, parameters: Optional[List[ProgramParameter]] = None, return_values: Optional[List[ProgramResult]] = None, @@ -306,7 +304,6 @@ def upload_program( not specified via `metadata`. is_public: Whether the runtime program should be visible to the public. description: Program description. Required if not specified via `metadata`. - version: Program version. The default is 1.0 if not specified. backend_requirements: Backend requirements. parameters: A list of program input parameters. return_values: A list of program return values. @@ -326,7 +323,7 @@ def upload_program( metadata=metadata, name=name, max_execution_time=max_execution_time, is_public=is_public, description=description, - version=version, backend_requirements=backend_requirements, + backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results) @@ -385,7 +382,7 @@ def _merge_metadata( initial[key] = val # TODO validate metadata format - metadata_keys = ['name', 'max_execution_time', 'description', 'version', + metadata_keys = ['name', 'max_execution_time', 'description', 'backend_requirements', 'parameters', 'return_values', 'interim_results', 'is_public'] return {key: val for key, val in initial.items() if key in metadata_keys} diff --git a/qiskit_ibm/runtime/program/program_metadata_sample.json b/qiskit_ibm/runtime/program/program_metadata_sample.json index a38c18fd6..5df24385f 100644 --- a/qiskit_ibm/runtime/program/program_metadata_sample.json +++ b/qiskit_ibm/runtime/program/program_metadata_sample.json @@ -2,7 +2,6 @@ "name": "runtime-simple", "description": "Simple runtime program used for testing.", "max_execution_time": 300, - "version": 1.0, "backend_requirements": {"min_num_qubits": 5}, "parameters": [ {"name": "iterations", "description": "Number of iterations to run. Each iteration generates a runs a random circuit.", "type": "integer", "required": true} diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index ee884146b..bdb503d46 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -51,7 +51,6 @@ def __init__( return_values: Optional[List] = None, interim_results: Optional[List] = None, max_execution_time: int = 0, - version: str = "0", backend_requirements: Optional[Dict] = None, creation_date: str = "", is_public: Optional[bool] = False @@ -66,7 +65,6 @@ def __init__( return_values: Documentation on program return values. interim_results: Documentation on program interim results. max_execution_time: Maximum execution time. - version: Program version. backend_requirements: Backend requirements. creation_date: Program creation date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. @@ -75,7 +73,6 @@ def __init__( self._id = program_id self._description = description self._max_execution_time = max_execution_time - self._version = version self._backend_requirements = backend_requirements or {} self._parameters: List[ProgramParameter] = [] self._return_values: List[ProgramResult] = [] @@ -114,7 +111,6 @@ def _format_common(items: List) -> None: formatted = [f'{self.program_id}:', f" Name: {self.name}", f" Description: {self.description}", - f" Version: {self.version}", f" Creation date: {self.creation_date}", f" Max execution time: {self.max_execution_time}", f" Input parameters:"] @@ -148,7 +144,6 @@ def to_dict(self) -> Dict: "name": self.name, "description": self.description, "max_execution_time": self.max_execution_time, - "version": self.version, "backend_requirements": self.backend_requirements, "parameters": self.parameters(), "return_values": self.return_values, @@ -227,15 +222,6 @@ def max_execution_time(self) -> int: """ return self._max_execution_time - @property - def version(self) -> str: - """Program version. - - Returns: - Program version. - """ - return self._version - @property def backend_requirements(self) -> Dict: """Backend requirements. diff --git a/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml new file mode 100644 index 000000000..099dcd222 --- /dev/null +++ b/releasenotes/notes/remove-version-field-0543061d4a7e059a.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Runtime programs will no longer have a ``version`` field. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 6711d5ee7..a041c718a 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -26,7 +26,7 @@ class BaseFakeProgram: """Base class for faking a program.""" - def __init__(self, program_id, name, data, cost, description, version="1.0", + def __init__(self, program_id, name, data, cost, description, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Initialize a fake program.""" @@ -35,7 +35,6 @@ def __init__(self, program_id, name, data, cost, description, version="1.0", self._data = data self._cost = cost self._description = description - self._version = version self._backend_requirements = backend_requirements self._parameters = parameters self._return_values = return_values @@ -48,7 +47,6 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'version': self._version, 'isPublic': self._is_public} if include_data: out['data'] = self._data @@ -237,7 +235,7 @@ def list_programs(self): programs.append(prog.to_dict()) return programs - def program_create(self, program_data, name, description, max_execution_time, version="1.0", + def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Create a program.""" @@ -249,7 +247,7 @@ def program_create(self, program_data, name, description, max_execution_time, ve raise RequestsApiError("Program already exists.", status_code=409) self._programs[program_id] = BaseFakeProgram( program_id=program_id, name=name, data=program_data, cost=max_execution_time, - description=description, version=version, backend_requirements=backend_requirements, + description=description, backend_requirements=backend_requirements, parameters=parameters, return_values=return_values, interim_results=interim_results, is_public=is_public) return {'id': program_id} diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index f60f027fb..b918eab08 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -70,7 +70,6 @@ class TestRuntime(IBMTestCase): "name": "qiskit-test", "description": "Test program.", "max_execution_time": 300, - "version": "0.1", "backend_requirements": {"min_num_qubits": 5}, "parameters": [ {'name': 'param1', 'description': 'Desc 1', 'type': 'str', 'required': True}, @@ -286,13 +285,13 @@ def test_print_programs(self): for prog in programs: self.assertIn(prog.program_id, stdout) self.assertIn(prog.name, stdout) - self.assertNotIn(prog.version, stdout) + self.assertNotIn(str(prog.max_execution_time), stdout) self.runtime.pprint_programs(detailed=True) stdout_detailed = mock_stdout.getvalue() for prog in programs: self.assertIn(prog.program_id, stdout_detailed) self.assertIn(prog.name, stdout_detailed) - self.assertIn(prog.version, stdout_detailed) + self.assertIn(str(prog.max_execution_time), stdout_detailed) def test_upload_program(self): """Test uploading a program.""" @@ -600,7 +599,6 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) - self.assertEqual(self.DEFAULT_METADATA["version"], program.version) self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], program.backend_requirements) self.assertEqual([ProgramParameter(**param) for param in @@ -615,12 +613,11 @@ def test_program_metadata(self): def test_metadata_combined(self): """Test combining metadata""" - update_metadata = {"version": "1.2", "max_execution_time": 600} + update_metadata = {"max_execution_time": 600} program_id = self.runtime.upload_program( data="def main() {}", metadata=self.DEFAULT_METADATA, **update_metadata) program = self.runtime.program(program_id) self.assertEqual(update_metadata['max_execution_time'], program.max_execution_time) - self.assertEqual(update_metadata["version"], program.version) def test_different_providers(self): """Test retrieving job submitted with different provider.""" diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 39a0cd2ff..34869db2a 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -626,7 +626,6 @@ def _validate_program(self, program): self.assertTrue(program.description) self.assertTrue(program.max_execution_time) self.assertTrue(program.creation_date) - self.assertTrue(program.version) def _upload_program( self, From 93a2b853b4dd7cb65429ee1fb406007bd27c2428 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 01:24:53 -0400 Subject: [PATCH 20/48] Rename isPublic to is_public when creating or reading runtime programs (#155) --- qiskit_ibm/api/rest/runtime.py | 2 +- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index da8cf3487..41e14765a 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -96,7 +96,7 @@ def create_program( 'cost': str(max_execution_time), 'description': description.encode(), 'max_execution_time': max_execution_time, - 'isPublic': is_public} + 'is_public': is_public} if backend_requirements: data['backendRequirements'] = json.dumps(backend_requirements) if parameters: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index febe967f6..776b8347d 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -196,7 +196,7 @@ def _to_program(self, response: Dict) -> RuntimeProgram: max_execution_time=response.get('cost', 0), creation_date=response.get('creationDate', ""), backend_requirements=backend_req, - is_public=response.get('isPublic', False)) + is_public=response.get('is_public', False)) def run( self, diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index a041c718a..1243fbb9a 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -47,7 +47,7 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'isPublic': self._is_public} + 'is_public': self._is_public} if include_data: out['data'] = self._data if self._backend_requirements: From 966d42aebccc50fd098225b3b7870a0c5eb140ab Mon Sep 17 00:00:00 2001 From: Renier Morales Date: Mon, 18 Oct 2021 10:16:48 -0500 Subject: [PATCH 21/48] Update programId to program_id when running program (#139) This needs to change in the program upload body request in order to meet the IBM Cloud API guidance. Co-authored-by: Jessie Yu --- qiskit_ibm/api/rest/runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 41e14765a..1adcc04cb 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -136,7 +136,7 @@ def program_run( """ url = self.get_url('jobs') payload = { - 'programId': program_id, + 'program_id': program_id, 'hub': hub, 'group': group, 'project': project, From 031fc2539aa9d14dbfe32e2d3c39617387d38698 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 11:30:48 -0400 Subject: [PATCH 22/48] Add support to view program update date (#160) --- qiskit_ibm/runtime/ibm_runtime_service.py | 3 ++- qiskit_ibm/runtime/runtime_program.py | 13 +++++++++++++ ...eature-program-update-date-7325797d7abd36ad.yaml | 5 +++++ test/ibm/runtime/fake_runtime_client.py | 4 +++- test/ibm/runtime/test_runtime.py | 2 ++ 5 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 776b8347d..cc552493e 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -194,7 +194,8 @@ def _to_program(self, response: Dict) -> RuntimeProgram: return_values=ret_vals, interim_results=interim_results, max_execution_time=response.get('cost', 0), - creation_date=response.get('creationDate', ""), + creation_date=response.get('creation_date', ""), + update_date=response.get('update_date', ""), backend_requirements=backend_req, is_public=response.get('is_public', False)) diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index bdb503d46..9b6cde746 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -53,6 +53,7 @@ def __init__( max_execution_time: int = 0, backend_requirements: Optional[Dict] = None, creation_date: str = "", + update_date: str = "", is_public: Optional[bool] = False ) -> None: """RuntimeProgram constructor. @@ -67,6 +68,7 @@ def __init__( max_execution_time: Maximum execution time. backend_requirements: Backend requirements. creation_date: Program creation date. + update_date: Program last updated date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. """ self._name = program_name @@ -78,6 +80,7 @@ def __init__( self._return_values: List[ProgramResult] = [] self._interim_results: List[ProgramResult] = [] self._creation_date = creation_date + self._update_date = update_date self._is_public = is_public if parameters: @@ -112,6 +115,7 @@ def _format_common(items: List) -> None: f" Name: {self.name}", f" Description: {self.description}", f" Creation date: {self.creation_date}", + f" Update date: {self.update_date}", f" Max execution time: {self.max_execution_time}", f" Input parameters:"] @@ -240,6 +244,15 @@ def creation_date(self) -> str: """ return self._creation_date + @property + def update_date(self) -> str: + """Program last updated date. + + Returns: + Program last updated date. + """ + return self._update_date + @property def is_public(self) -> bool: """Whether the program is visible to all. diff --git a/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml new file mode 100644 index 000000000..6fe46c1df --- /dev/null +++ b/releasenotes/notes/feature-program-update-date-7325797d7abd36ad.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can view the last updated date of a runtime program using + :attr:`~qiskit_ibm.runtime.RuntimeProgram.update_date` property. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 1243fbb9a..f5990abed 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -47,7 +47,9 @@ def to_dict(self, include_data=False): 'name': self._name, 'cost': self._cost, 'description': self._description, - 'is_public': self._is_public} + 'is_public': self._is_public, + 'creation_date': '2021-09-13T17:27:42Z', + 'update_date': '2021-09-14T19:25:32Z'} if include_data: out['data'] = self._data if self._backend_requirements: diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index b918eab08..acedbea2f 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -599,6 +599,8 @@ def test_program_metadata(self): self.assertEqual(self.DEFAULT_METADATA['description'], program.description) self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], program.max_execution_time) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], program.backend_requirements) self.assertEqual([ProgramParameter(**param) for param in From 1561a9b521632f9a0de4eaac71a5c0736401c3fd Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 22:39:30 -0400 Subject: [PATCH 23/48] Upload runtime program using 'data' field (#157) --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 9 ++++----- qiskit_ibm/runtime/ibm_runtime_service.py | 4 +++- test/ibm/runtime/fake_runtime_client.py | 3 --- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 05b8264d0..d13136efb 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -49,7 +49,7 @@ def list_programs(self) -> List[Dict]: def program_create( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, @@ -63,7 +63,7 @@ def program_create( Args: name: Name of the program. - program_data: Program data. + program_data: Program data (base64 encoded). description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 1adcc04cb..e04cacbda 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -65,7 +65,7 @@ def list_programs(self) -> List[Dict]: def create_program( self, - program_data: bytes, + program_data: str, name: str, description: str, max_execution_time: int, @@ -78,7 +78,7 @@ def create_program( """Upload a new program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). name: Name of the program. description: Program description. max_execution_time: Maximum execution time. @@ -93,6 +93,7 @@ def create_program( """ url = self.get_url('programs') data = {'name': name, + 'data': program_data, 'cost': str(max_execution_time), 'description': description.encode(), 'max_execution_time': max_execution_time, @@ -105,9 +106,7 @@ def create_program( data['returnValues'] = json.dumps(return_values) if interim_results: data['interimResults'] = json.dumps(interim_results) - - files = {'program': (name, program_data)} # type: ignore[dict-item] - response = self.session.post(url, data=data, files=files).json() + response = self.session.post(url, data=data).json() return response def program_run( diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index cc552493e..ab21ec87e 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -12,6 +12,7 @@ """Qiskit runtime service.""" +import base64 import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json @@ -338,7 +339,8 @@ def upload_program( data = file.read() try: - response = self._api_client.program_create(program_data=data.encode(), + program_data = base64.b64encode(data.encode('utf-8')).decode('utf-8') + response = self._api_client.program_create(program_data=program_data, **program_metadata) except RequestsApiError as ex: if ex.status_code == 409: diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index f5990abed..65b433987 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -241,9 +241,6 @@ def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, interim_results=None, is_public=False): """Create a program.""" - if isinstance(program_data, str): - with open(program_data, 'rb') as file: - program_data = file.read() program_id = name if program_id in self._programs: raise RequestsApiError("Program already exists.", status_code=409) From b656eced8c155fd782b0d636953153071ef68958 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 18 Oct 2021 22:41:26 -0400 Subject: [PATCH 24/48] Read programs from "programs" array in response (#161) --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 2 +- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index d13136efb..4aae5de02 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -13,7 +13,7 @@ """Client for accessing IBM Quantum runtime service.""" import logging -from typing import List, Dict, Union, Optional +from typing import Any, List, Dict, Union, Optional from qiskit_ibm.credentials import Credentials from qiskit_ibm.api.session import RetrySession @@ -39,7 +39,7 @@ def __init__( **credentials.connection_parameters()) self.api = Runtime(self._session) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index e04cacbda..8ae956dc2 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -54,7 +54,7 @@ def program_job(self, job_id: str) -> 'ProgramJob': """ return ProgramJob(self.session, job_id) - def list_programs(self) -> List[Dict]: + def list_programs(self) -> Dict[str, Any]: """Return a list of runtime programs. Returns: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index ab21ec87e..185df03dd 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -140,7 +140,7 @@ def programs(self, refresh: bool = False) -> List[RuntimeProgram]: if not self._programs or refresh: self._programs = {} response = self._api_client.list_programs() - for prog_dict in response: + for prog_dict in response.get("programs", []): program = self._to_program(prog_dict) self._programs[program.program_id] = program return list(self._programs.values()) diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 65b433987..3a05e45f9 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -235,7 +235,7 @@ def list_programs(self): programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) - return programs + return {"programs": programs} def program_create(self, program_data, name, description, max_execution_time, backend_requirements=None, parameters=None, return_values=None, From 8fb3669311aa5bb346bc4904e877101375d87525 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 20 Oct 2021 23:26:53 -0400 Subject: [PATCH 25/48] Pass program as base64 string to update (#168) --- qiskit_ibm/api/clients/runtime.py | 2 +- qiskit_ibm/api/rest/runtime.py | 4 ++-- qiskit_ibm/runtime/ibm_runtime_service.py | 17 +++++++++++++---- qiskit_ibm/runtime/utils.py | 12 ++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 4aae5de02..ddc71bff9 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -158,7 +158,7 @@ def program_update(self, program_id: str, program_data: str) -> None: Args: program_id: Program ID. - program_data: Program data. + program_data: Program data (base64 encoded). """ self.api.program(program_id).update(program_data) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 8ae956dc2..d5ceff21e 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -238,11 +238,11 @@ def update(self, program_data: str) -> None: """Update a program. Args: - program_data: Program data. + program_data: Program data (base64 encoded). """ url = self.get_url("data") self.session.put(url, data=program_data, - headers={'Content-Type': 'text/plain'}) + headers={'Content-Type': 'application/octet-stream'}) class ProgramJob(RestAdapterBase): diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 185df03dd..cd6a45897 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -12,7 +12,6 @@ """Qiskit runtime service.""" -import base64 import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json @@ -24,7 +23,7 @@ from .runtime_job import RuntimeJob from .runtime_program import RuntimeProgram, ProgramParameter, ProgramResult, ParameterNamespace -from .utils import RuntimeEncoder, RuntimeDecoder +from .utils import RuntimeEncoder, RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) from .program.result_decoder import ResultDecoder @@ -339,7 +338,7 @@ def upload_program( data = file.read() try: - program_data = base64.b64encode(data.encode('utf-8')).decode('utf-8') + program_data = to_base64_string(data) response = self._api_client.program_create(program_data=program_data, **program_metadata) except RequestsApiError as ex: @@ -412,12 +411,22 @@ def update_program( Args: program_id: Program ID. data: Program data or path of the file containing program data to upload. + + Raises: + RuntimeProgramNotFound: If the program doesn't exist. + QiskitRuntimeError: If the request failed. """ if "def main(" not in data: # This is the program file with open(data, "r") as file: data = file.read() - self._api_client.program_update(program_id, data) + try: + program_data = to_base64_string(data) + self._api_client.program_update(program_id, program_data) + except RequestsApiError as ex: + if ex.status_code == 404: + raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise QiskitRuntimeError(f"Failed to update program: {ex}") from None def delete_program(self, program_id: str) -> None: """Delete a runtime program. diff --git a/qiskit_ibm/runtime/utils.py b/qiskit_ibm/runtime/utils.py index 5b46f00ca..b0e4c2a57 100644 --- a/qiskit_ibm/runtime/utils.py +++ b/qiskit_ibm/runtime/utils.py @@ -40,6 +40,18 @@ from qiskit.result import Result +def to_base64_string(data: str) -> str: + """Convert string to base64 string. + + Args: + data: string to convert + + Returns: + data as base64 string + """ + return base64.b64encode(data.encode('utf-8')).decode('utf-8') + + def _serialize_and_encode( data: Any, serializer: Callable, From 74472c5d30ab7c721136b37b08972b6e0cd5ca17 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Mon, 25 Oct 2021 19:16:16 -0400 Subject: [PATCH 26/48] Accept JSON schema as program metadata (#158) * Accept JSON schema as program metadata * Update qiskit_ibm/runtime/ibm_runtime_service.py Co-authored-by: Jessie Yu * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Jessie Yu Co-authored-by: Jessie Yu --- qiskit_ibm/api/clients/runtime.py | 14 +- qiskit_ibm/api/rest/runtime.py | 20 +-- qiskit_ibm/runtime/ibm_runtime_service.py | 109 ++++++--------- .../program/program_metadata_sample.json | 45 +++++-- qiskit_ibm/runtime/runtime_program.py | 124 ++++++++---------- ...metadata-json-schema-46f034ada7443cf9.yaml | 10 ++ test/ibm/runtime/fake_runtime_client.py | 18 ++- test/ibm/runtime/test_runtime.py | 85 +++++++----- 8 files changed, 208 insertions(+), 217 deletions(-) create mode 100644 releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index ddc71bff9..9844f2b76 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -54,10 +54,7 @@ def program_create( description: str, max_execution_time: int, is_public: Optional[bool] = False, - backend_requirements: Optional[Dict] = None, - parameters: Optional[Dict] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None + spec: Optional[Dict] = None ) -> Dict: """Create a new program. @@ -67,10 +64,7 @@ def program_create( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - backend_requirements: Backend requirements. - parameters: Program parameters. - return_values: Program return values. - interim_results: Program interim results. + spec: Backend requirements, parameters, interim results, return values, etc. Returns: Server response. @@ -79,9 +73,7 @@ def program_create( program_data=program_data, name=name, description=description, max_execution_time=max_execution_time, - is_public=is_public, backend_requirements=backend_requirements, - parameters=parameters, return_values=return_values, - interim_results=interim_results + is_public=is_public, spec=spec ) def program_get(self, program_id: str) -> Dict: diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index d5ceff21e..01626efee 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -70,10 +70,7 @@ def create_program( description: str, max_execution_time: int, is_public: Optional[bool] = False, - backend_requirements: Optional[Dict] = None, - parameters: Optional[Dict] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None + spec: Optional[Dict] = None ) -> Dict: """Upload a new program. @@ -83,10 +80,7 @@ def create_program( description: Program description. max_execution_time: Maximum execution time. is_public: Whether the program should be public. - backend_requirements: Backend requirements. - parameters: Program parameters. - return_values: Program return values. - interim_results: Program interim results. + spec: Backend requirements, parameters, interim results, return values, etc. Returns: JSON response. @@ -98,14 +92,8 @@ def create_program( 'description': description.encode(), 'max_execution_time': max_execution_time, 'is_public': is_public} - if backend_requirements: - data['backendRequirements'] = json.dumps(backend_requirements) - if parameters: - data['parameters'] = json.dumps({"doc": parameters}) - if return_values: - data['returnValues'] = json.dumps(return_values) - if interim_results: - data['interimResults'] = json.dumps(interim_results) + if spec is not None: + data['spec'] = json.dumps(spec) response = self.session.post(url, data=data).json() return response diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index cd6a45897..a680afd49 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -15,14 +15,13 @@ import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json -import copy import re from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit_ibm import ibm_provider # pylint: disable=unused-import from .runtime_job import RuntimeJob -from .runtime_program import RuntimeProgram, ProgramParameter, ProgramResult, ParameterNamespace +from .runtime_program import RuntimeProgram, ParameterNamespace from .utils import RuntimeEncoder, RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) @@ -182,21 +181,26 @@ def _to_program(self, response: Dict) -> RuntimeProgram: Returns: A ``RuntimeProgram`` instance. """ - backend_req = json.loads(response.get('backendRequirements', '{}')) - params = json.loads(response.get('parameters', '{}')).get("doc", []) - ret_vals = json.loads(response.get('returnValues', '{}')) - interim_results = json.loads(response.get('interimResults', '{}')) + backend_requirements = {} + parameters = {} + return_values = {} + interim_results = {} + if "spec" in response: + backend_requirements = response["spec"].get('backend_requirements', {}) + parameters = response["spec"].get('parameters', {}) + return_values = response["spec"].get('return_values', {}) + interim_results = response["spec"].get('interim_results', {}) return RuntimeProgram(program_name=response['name'], program_id=response['id'], description=response.get('description', ""), - parameters=params, - return_values=ret_vals, + parameters=parameters, + return_values=return_values, interim_results=interim_results, max_execution_time=response.get('cost', 0), creation_date=response.get('creation_date', ""), update_date=response.get('update_date', ""), - backend_requirements=backend_req, + backend_requirements=backend_requirements, is_public=response.get('is_public', False)) def run( @@ -267,15 +271,7 @@ def run( def upload_program( self, data: str, - metadata: Optional[Union[Dict, str]] = None, - name: Optional[str] = None, - is_public: Optional[bool] = False, - max_execution_time: Optional[int] = None, - description: Optional[str] = None, - backend_requirements: Optional[str] = None, - parameters: Optional[List[ProgramParameter]] = None, - return_values: Optional[List[ProgramResult]] = None, - interim_results: Optional[List[ProgramResult]] = None + metadata: Optional[Union[Dict, str]] = None ) -> str: """Upload a runtime program. @@ -298,17 +294,23 @@ def upload_program( Args: data: Program data or path of the file containing program data to upload. metadata: Name of the program metadata file or metadata dictionary. - A metadata file needs to be in the JSON format. - See :file:`program/program_metadata_sample.yaml` for an example. - name: Name of the program. Required if not specified via `metadata`. - max_execution_time: Maximum execution time in seconds. Required if - not specified via `metadata`. - is_public: Whether the runtime program should be visible to the public. - description: Program description. Required if not specified via `metadata`. - backend_requirements: Backend requirements. - parameters: A list of program input parameters. - return_values: A list of program return values. - interim_results: A list of program interim results. + A metadata file needs to be in the JSON format. The ``parameters``, + ``return_values``, and ``interim_results`` should be defined as JSON Schema. + See :file:`program/program_metadata_sample.json` for an example. The + fields in metadata are explained below. + + * name: Name of the program. Required. + * max_execution_time: Maximum execution time in seconds. Required. + * description: Program description. Required. + * is_public: Whether the runtime program should be visible to the public. + The default is ``False``. + * spec: Specifications for backend characteristics and input parameters + required to run the program, interim results and final result. + + * backend_requirements: Backend requirements. + * parameters: Program input parameters in JSON schema format. + * return_values: Program return values in JSON schema format. + * interim_results: Program interim results in JSON schema format. Returns: Program ID. @@ -319,14 +321,7 @@ def upload_program( IBMNotAuthorizedError: If you are not authorized to upload programs. QiskitRuntimeError: If the upload failed. """ - program_metadata = self._merge_metadata( - initial={}, - metadata=metadata, - name=name, max_execution_time=max_execution_time, - is_public=is_public, description=description, - backend_requirements=backend_requirements, - parameters=parameters, - return_values=return_values, interim_results=interim_results) + program_metadata = self._read_metadata(metadata=metadata) for req in ['name', 'description', 'max_execution_time']: if req not in program_metadata or not program_metadata[req]: @@ -351,21 +346,17 @@ def upload_program( raise QiskitRuntimeError(f"Failed to create program: {ex}") from None return response['id'] - def _merge_metadata( + def _read_metadata( self, - initial: Dict, - metadata: Optional[Union[Dict, str]] = None, - **kwargs: Any + metadata: Optional[Union[Dict, str]] = None ) -> Dict: - """Merge multiple copies of metadata. + """Read metadata. Args: - initial: The initial metadata. This may be mutated. metadata: Name of the program metadata file or metadata dictionary. - **kwargs: Additional metadata fields to overwrite. Returns: - Merged metadata. + Return metadata. """ upd_metadata: dict = {} if metadata is not None: @@ -373,33 +364,11 @@ def _merge_metadata( with open(metadata, 'r') as file: upd_metadata = json.load(file) else: - upd_metadata = copy.deepcopy(metadata) - - self._tuple_to_dict(initial) - initial.update(upd_metadata) - - self._tuple_to_dict(kwargs) - for key, val in kwargs.items(): - if val is not None: - initial[key] = val - + upd_metadata = metadata # TODO validate metadata format metadata_keys = ['name', 'max_execution_time', 'description', - 'backend_requirements', 'parameters', 'return_values', - 'interim_results', 'is_public'] - return {key: val for key, val in initial.items() if key in metadata_keys} - - def _tuple_to_dict(self, metadata: Dict) -> None: - """Convert fields in metadata from named tuples to dictionaries. - - Args: - metadata: Metadata to be converted. - """ - for key in ['parameters', 'return_values', 'interim_results']: - doc_list = metadata.pop(key, None) - if not doc_list or isinstance(doc_list[0], dict): - continue - metadata[key] = [dict(elem._asdict()) for elem in doc_list] + 'spec', 'is_public'] + return {key: val for key, val in upd_metadata.items() if key in metadata_keys} def update_program( self, diff --git a/qiskit_ibm/runtime/program/program_metadata_sample.json b/qiskit_ibm/runtime/program/program_metadata_sample.json index 5df24385f..6449b474b 100644 --- a/qiskit_ibm/runtime/program/program_metadata_sample.json +++ b/qiskit_ibm/runtime/program/program_metadata_sample.json @@ -2,15 +2,38 @@ "name": "runtime-simple", "description": "Simple runtime program used for testing.", "max_execution_time": 300, - "backend_requirements": {"min_num_qubits": 5}, - "parameters": [ - {"name": "iterations", "description": "Number of iterations to run. Each iteration generates a runs a random circuit.", "type": "integer", "required": true} - ], - "return_values": [ - {"name": "-", "description": "A string that says 'All done!'.", "type": "string"} - ], - "interim_results": [ - {"name": "iteration", "description": "Iteration number.", "type": "int"}, - {"name": "counts", "description": "Histogram data of the circuit result.", "type": "dict"} - ] + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "type": "object", + "properties": { + "iterations": { + "description": "Number of iterations to run. Each iteration generates and runs a random circuit.", + "type": "integer" + } + }, + "required": [ + "iterations" + ] + }, + "return_values": { + "type": "string", + "description": "A string that says 'All done!'." + }, + "interim_results": { + "type": "object", + "properties": { + "iteration": { + "description": "Iteration number.", + "type": "integer" + }, + "counts": { + "description": "Histogram data of the circuit result.", + "type": "object" + } + } + } + } } \ No newline at end of file diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index 9b6cde746..5e648244e 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -13,7 +13,8 @@ """Qiskit runtime program.""" import logging -from typing import Optional, List, NamedTuple, Dict +import re +from typing import Optional, List, Dict from types import SimpleNamespace from qiskit_ibm.exceptions import IBMInputValueError @@ -47,9 +48,9 @@ def __init__( program_name: str, program_id: str, description: str, - parameters: Optional[List] = None, - return_values: Optional[List] = None, - interim_results: Optional[List] = None, + parameters: Optional[Dict] = None, + return_values: Optional[Dict] = None, + interim_results: Optional[Dict] = None, max_execution_time: int = 0, backend_requirements: Optional[Dict] = None, creation_date: str = "", @@ -76,49 +77,44 @@ def __init__( self._description = description self._max_execution_time = max_execution_time self._backend_requirements = backend_requirements or {} - self._parameters: List[ProgramParameter] = [] - self._return_values: List[ProgramResult] = [] - self._interim_results: List[ProgramResult] = [] + self._parameters = parameters or {} + self._return_values = return_values or {} + self._interim_results = interim_results or {} self._creation_date = creation_date self._update_date = update_date self._is_public = is_public - if parameters: - for param in parameters: - self._parameters.append( - ProgramParameter(name=param['name'], - description=param['description'], - type=param['type'], - required=param['required'])) - if return_values is not None: - for ret in return_values: - self._return_values.append(ProgramResult(name=ret['name'], - description=ret['description'], - type=ret['type'])) - if interim_results is not None: - for intret in interim_results: - self._interim_results.append(ProgramResult(name=intret['name'], - description=intret['description'], - type=intret['type'])) - def __str__(self) -> str: - def _format_common(items: List) -> None: - """Add name, description, and type to `formatted`.""" - for item in items: - formatted.append(" "*4 + "- " + item.name + ":") - formatted.append(" "*6 + "Description: " + item.description) - formatted.append(" "*6 + "Type: " + item.type) - if hasattr(item, 'required'): - formatted.append(" "*6 + "Required: " + str(item.required)) + def _format_common(schema: Dict) -> None: + """Add title, description and property details to `formatted`.""" + if "description" in schema: + formatted.append(" "*4 + "Description: {}".format(schema["description"])) + if "type" in schema: + formatted.append(" "*4 + "Type: {}".format(str(schema["type"]))) + if "properties" in schema: + formatted.append(" "*4 + "Properties:") + for property_name, property_value in schema["properties"].items(): + formatted.append(" "*8 + "- " + property_name + ":") + for key, value in property_value.items(): + formatted.append(" "*12 + "{}: {}".format(sentence_case(key), str(value))) + formatted.append(" "*12 + "Required: " + + str(property_name in schema.get("required", []))) + + def sentence_case(camel_case_text: str) -> str: + """Converts camelCase to Sentence case""" + if camel_case_text == '': + return camel_case_text + sentence_case_text = re.sub('([A-Z])', r' \1', camel_case_text) + return sentence_case_text[:1].upper() + sentence_case_text[1:].lower() formatted = [f'{self.program_id}:', f" Name: {self.name}", f" Description: {self.description}", f" Creation date: {self.creation_date}", f" Update date: {self.update_date}", - f" Max execution time: {self.max_execution_time}", - f" Input parameters:"] + f" Max execution time: {self.max_execution_time}"] + formatted.append(" Input parameters:") if self._parameters: _format_common(self._parameters) else: @@ -198,7 +194,7 @@ def description(self) -> str: return self._description @property - def return_values(self) -> List['ProgramResult']: + def return_values(self) -> Dict: """Program return value definitions. Returns: @@ -207,7 +203,7 @@ def return_values(self) -> List['ProgramResult']: return self._return_values @property - def interim_results(self) -> List['ProgramResult']: + def interim_results(self) -> Dict: """Program interim result definitions. Returns: @@ -263,21 +259,6 @@ def is_public(self) -> bool: return self._is_public -class ProgramParameter(NamedTuple): - """Program parameter.""" - name: str - description: str - type: str - required: bool - - -class ProgramResult(NamedTuple): - """Program result.""" - name: str - description: str - type: str - - class ParameterNamespace(SimpleNamespace): """ A namespace for program parameters with validation. @@ -285,26 +266,26 @@ class ParameterNamespace(SimpleNamespace): and validation support. """ - def __init__(self, params: List[ProgramParameter]): + def __init__(self, parameters: Dict): """ParameterNamespace constructor. Args: - params: The program's input parameters. + parameters: The program's input parameters. """ super().__init__() - # Allow access to the raw program parameters list - self.__metadata = params + # Allow access to the raw program parameters dict + self.__metadata = parameters # For localized logic, create store of parameters in dictionary self.__program_params: dict = {} - for param in params: + for parameter_name, parameter_value in parameters.get("properties", {}).items(): # (1) Add parameters to a dict by name - setattr(self, param.name, None) + setattr(self, parameter_name, None) # (2) Store the program params for validation - self.__program_params[param.name] = param + self.__program_params[parameter_name] = parameter_value @property - def metadata(self) -> List[ProgramParameter]: + def metadata(self) -> Dict: """Returns the parameter metadata""" return self.__metadata @@ -320,12 +301,12 @@ def validate(self) -> None: """ # Iterate through the user's stored inputs - for param_name, program_param in self.__program_params.items(): - # Set invariants: User-specified parameter value (value) and whether it's required (req) - value = getattr(self, param_name, None) + for parameter_name, parameter_value in self.__program_params.items(): + # Set invariants: User-specified parameter value (value) and if it's required (req) + value = getattr(self, parameter_name, None) # Check there exists a program parameter of that name. - if value is None and program_param.required: - raise IBMInputValueError('Param (%s) missing required value!' % param_name) + if value is None and parameter_name in self.metadata.get("required", []): + raise IBMInputValueError('Param (%s) missing required value!' % parameter_name) def __str__(self) -> str: """Creates string representation of object""" @@ -338,15 +319,14 @@ def __str__(self) -> str: 'Required', 'Description' ) - # List of ProgramParameter objects (str) params_str = '\n'.join([ '| {:10.10} | {:12.12} | {:12.12}| {:8.8} | {:>15} |'.format( - param.name, - str(getattr(self, param.name, 'None')), - param.type, - str(param.required), - param.description - ) for param in self.__program_params.values()]) + parameter_name, + str(getattr(self, parameter_name, "None")), + str(parameter_value.get("type", "None")), + str(parameter_name in self.metadata.get("required", [])), + str(parameter_value.get("description", "None")) + ) for parameter_name, parameter_value in self.__program_params.items()]) return "ParameterNamespace (Values):\n%s\n%s\n%s" \ % (header, '-' * len(header), params_str) diff --git a/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml b/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml new file mode 100644 index 000000000..08eb17d93 --- /dev/null +++ b/releasenotes/notes/upgrade-metadata-json-schema-46f034ada7443cf9.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + :meth:`qiskit_ibm.runtime.IBMRuntimeService.upload_program` now takes only two parameters, + ``data``, which is the program passed as a string or the path to the program file and the + ``metadata``, which is passed as a dictionary or path to the metadata JSON file. + In ``metadata`` the ``backend_requirements``, ``parameters``, ``return_values`` and + ``interim_results`` are now grouped under a specifications ``spec`` section. + ``parameters``, ``return_values`` and ``interim_results`` should now be specified as + JSON Schema. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 3a05e45f9..bd2b4852d 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -52,14 +52,15 @@ def to_dict(self, include_data=False): 'update_date': '2021-09-14T19:25:32Z'} if include_data: out['data'] = self._data + out['spec'] = {} if self._backend_requirements: - out['backendRequirements'] = json.dumps(self._backend_requirements) + out['spec']['backend_requirements'] = self._backend_requirements if self._parameters: - out['parameters'] = json.dumps({"doc": self._parameters}) + out['spec']['parameters'] = self._parameters if self._return_values: - out['returnValues'] = json.dumps(self._return_values) + out['spec']['return_values'] = self._return_values if self._interim_results: - out['interimResults'] = json.dumps(self._interim_results) + out['spec']['interim_results'] = self._interim_results return out @@ -231,19 +232,22 @@ def set_final_status(self, final_status): self._final_status = final_status def list_programs(self): - """List all progrmas.""" + """List all programs.""" programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) return {"programs": programs} def program_create(self, program_data, name, description, max_execution_time, - backend_requirements=None, parameters=None, return_values=None, - interim_results=None, is_public=False): + spec=None, is_public=False): """Create a program.""" program_id = name if program_id in self._programs: raise RequestsApiError("Program already exists.", status_code=409) + backend_requirements = spec.get('backend_requirements', None) + parameters = spec.get('parameters', None) + return_values = spec.get('return_values', None) + interim_results = spec.get('interim_results', None) self._programs[program_id] = BaseFakeProgram( program_id=program_id, name=name, data=program_data, cost=max_execution_time, description=description, backend_requirements=backend_requirements, diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index acedbea2f..954755b43 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -12,6 +12,7 @@ """Tests for runtime service.""" +import copy import json import os from io import StringIO @@ -55,7 +56,7 @@ from qiskit_ibm.runtime import IBMRuntimeService, RuntimeJob from qiskit_ibm.runtime.constants import API_TO_JOB_ERROR_MESSAGE from qiskit_ibm.runtime.exceptions import RuntimeProgramNotFound, RuntimeJobFailureError -from qiskit_ibm.runtime.runtime_program import ParameterNamespace, ProgramParameter, ProgramResult +from qiskit_ibm.runtime.runtime_program import ParameterNamespace from ...ibm_test_case import IBMTestCase from .fake_runtime_client import (BaseFakeRuntimeClient, FailedRanTooLongRuntimeJob, @@ -70,16 +71,50 @@ class TestRuntime(IBMTestCase): "name": "qiskit-test", "description": "Test program.", "max_execution_time": 300, - "backend_requirements": {"min_num_qubits": 5}, - "parameters": [ - {'name': 'param1', 'description': 'Desc 1', 'type': 'str', 'required': True}, - {'name': 'param2', 'description': 'Desc 2', 'type': 'int', 'required': False}], - "return_values": [ - {"name": "ret_val", "description": "Some return value.", "type": "string"} - ], - "interim_results": [ - {"name": "int_res", "description": "Some interim result", "type": "string"}, - ] + "spec": { + "backend_requirements": { + "min_num_qubits": 5 + }, + "parameters": { + "properties": { + "param1": { + "description": "Desc 1", + "type": "string", + "enum": [ + "a", + "b", + "c" + ] + }, + "param2": { + "description": "Desc 2", + "type": "integer", + "min": 0 + } + }, + "required": [ + "param1" + ] + }, + "return_values": { + "type": "object", + "description": "Return values", + "properties": { + "ret_val": { + "description": "Some return value.", + "type": "string" + } + } + }, + "interim_results": { + "properties": { + "int_res": { + "description": "Some interim result", + "type": "string" + } + } + } + } } def setUp(self): @@ -601,26 +636,15 @@ def test_program_metadata(self): program.max_execution_time) self.assertTrue(program.creation_date) self.assertTrue(program.update_date) - self.assertEqual(self.DEFAULT_METADATA['backend_requirements'], + self.assertEqual(self.DEFAULT_METADATA['spec']['backend_requirements'], program.backend_requirements) - self.assertEqual([ProgramParameter(**param) for param in - self.DEFAULT_METADATA['parameters']], + self.assertEqual(self.DEFAULT_METADATA['spec']['parameters'], program.parameters().metadata) - self.assertEqual([ProgramResult(**ret) for ret in - self.DEFAULT_METADATA['return_values']], + self.assertEqual(self.DEFAULT_METADATA['spec']['return_values'], program.return_values) - self.assertEqual([ProgramResult(**ret) for ret in - self.DEFAULT_METADATA['interim_results']], + self.assertEqual(self.DEFAULT_METADATA['spec']['interim_results'], program.interim_results) - def test_metadata_combined(self): - """Test combining metadata""" - update_metadata = {"max_execution_time": 600} - program_id = self.runtime.upload_program( - data="def main() {}", metadata=self.DEFAULT_METADATA, **update_metadata) - program = self.runtime.program(program_id) - self.assertEqual(update_metadata['max_execution_time'], program.max_execution_time) - def test_different_providers(self): """Test retrieving job submitted with different provider.""" program_id = self._upload_program() @@ -636,12 +660,13 @@ def _upload_program(self, name=None, max_execution_time=300, """Upload a new program.""" name = name or uuid.uuid4().hex data = "def main() {}" + metadata = copy.deepcopy(self.DEFAULT_METADATA) + metadata.update(name=name) + metadata.update(is_public=is_public) + metadata.update(max_execution_time=max_execution_time) program_id = self.runtime.upload_program( - name=name, data=data, - is_public=is_public, - max_execution_time=max_execution_time, - description="A test program") + metadata=metadata) return program_id def _run_program(self, program_id=None, inputs=None, job_classes=None, final_status=None, From ee76158d5b27011bdf6b40b54e547a14d676bd91 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 27 Oct 2021 15:43:37 -0400 Subject: [PATCH 27/48] Pass program params as object (#171) --- qiskit_ibm/api/clients/runtime.py | 2 +- qiskit_ibm/api/rest/runtime.py | 7 ++++--- qiskit_ibm/runtime/ibm_runtime_service.py | 5 ++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 9844f2b76..01ff6aff2 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -117,7 +117,7 @@ def program_run( program_id: str, credentials: Credentials, backend_name: str, - params: str, + params: Dict, image: str ) -> Dict: """Run the specified program. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 01626efee..f168ebf4a 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -19,6 +19,7 @@ from .base import RestAdapterBase from ..session import RetrySession +from ...runtime.utils import RuntimeEncoder logger = logging.getLogger(__name__) @@ -104,7 +105,7 @@ def program_run( group: str, project: str, backend_name: str, - params: str, + params: Dict, image: str ) -> Dict: """Execute the program. @@ -128,10 +129,10 @@ def program_run( 'group': group, 'project': project, 'backend': backend_name, - 'params': [params], + 'params': params, 'runtime': image } - data = json.dumps(payload) + data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> Dict: diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index a680afd49..374fd8020 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -22,7 +22,7 @@ from .runtime_job import RuntimeJob from .runtime_program import RuntimeProgram, ParameterNamespace -from .utils import RuntimeEncoder, RuntimeDecoder, to_base64_string +from .utils import RuntimeDecoder, to_base64_string from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound) from .program.result_decoder import ResultDecoder @@ -250,12 +250,11 @@ def run( raise IBMInputValueError('"image" needs to be in form of image_name:tag') backend_name = options['backend_name'] - params_str = json.dumps(inputs, cls=RuntimeEncoder) result_decoder = result_decoder or ResultDecoder response = self._api_client.program_run(program_id=program_id, credentials=self._provider.credentials, backend_name=backend_name, - params=params_str, + params=inputs, image=image) backend = self._provider.get_backend(backend_name) From da5227116d5b869882ecbc9e47e9e1247ce6b8a0 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 27 Oct 2021 14:50:50 -0400 Subject: [PATCH 28/48] Fix integration tests --- qiskit_ibm/api/rest/runtime.py | 17 +++++++------- qiskit_ibm/runtime/ibm_runtime_service.py | 5 ++-- test/ibm/runtime/test_runtime_integration.py | 24 ++++++++------------ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index f168ebf4a..7e774b978 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -87,16 +87,15 @@ def create_program( JSON response. """ url = self.get_url('programs') - data = {'name': name, - 'data': program_data, - 'cost': str(max_execution_time), - 'description': description.encode(), - 'max_execution_time': max_execution_time, - 'is_public': is_public} + payload = {'name': name, + 'data': program_data, + 'cost': max_execution_time, + 'description': description, + 'is_public': is_public} if spec is not None: - data['spec'] = json.dumps(spec) - response = self.session.post(url, data=data).json() - return response + payload['spec'] = spec + data = json.dumps(payload) + return self.session.post(url, data=data).json() def program_run( self, diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 374fd8020..7967fe039 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -120,8 +120,9 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None if detailed: print(str(prog)) else: - print(f"Name: {prog.name}") - print(f"Description: {prog.description}") + print(f"{prog.program_id}:",) + print(f" Name: {prog.name}") + print(f" Description: {prog.description}") def programs(self, refresh: bool = False) -> List[RuntimeProgram]: """Return available runtime programs. diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 34869db2a..101440c19 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -12,6 +12,7 @@ """Tests for runtime service.""" +import copy import unittest import os import uuid @@ -85,11 +86,12 @@ def setUpClass(cls, backend): cls.backend = backend cls.poll_time = 1 if backend.configuration().simulator else 5 cls.provider = backend.provider() + metadata = copy.deepcopy(cls.RUNTIME_PROGRAM_METADATA) + metadata['name'] = cls._get_program_name() try: cls.program_id = cls.provider.runtime.upload_program( - name=cls._get_program_name(), data=cls.RUNTIME_PROGRAM, - metadata=cls.RUNTIME_PROGRAM_METADATA) + metadata=metadata) except RuntimeDuplicateProgramError: pass except IBMNotAuthorizedError: @@ -200,13 +202,6 @@ def test_set_visibility(self): # Verify changed self.assertNotEqual(start_vis, end_vis) - def test_upload_program_conflict(self): - """Test uploading a program with conflicting name.""" - name = self._get_program_name() - self._upload_program(name=name) - with self.assertRaises(RuntimeDuplicateProgramError): - self._upload_program(name=name) - def test_delete_program(self): """Test deleting program.""" program_id = self._upload_program() @@ -626,6 +621,7 @@ def _validate_program(self, program): self.assertTrue(program.description) self.assertTrue(program.max_execution_time) self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) def _upload_program( self, @@ -636,13 +632,13 @@ def _upload_program( """Upload a new program.""" name = name or self._get_program_name() data = data or self.RUNTIME_PROGRAM + metadata = copy.deepcopy(self.RUNTIME_PROGRAM_METADATA) + metadata['name'] = name + metadata['max_execution_time'] = max_execution_time + metadata['is_public'] = is_public program_id = self.provider.runtime.upload_program( - name=name, data=data, - is_public=is_public, - metadata=self.RUNTIME_PROGRAM_METADATA, - max_execution_time=max_execution_time, - description="Qiskit test program") + metadata=metadata) self.to_delete.append(program_id) return program_id From f3033b8bb3d3ede5a019a7b224239c936b95b68d Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Thu, 28 Oct 2021 14:28:21 -0400 Subject: [PATCH 29/48] Use count to reduce one last extra call to API (#172) --- qiskit_ibm/runtime/ibm_runtime_service.py | 19 +++++++++++++------ test/ibm/runtime/fake_runtime_client.py | 4 +++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 7967fe039..55718335c 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -477,17 +477,24 @@ def jobs( """ job_responses = [] # type: List[Dict[str, Any]] current_page_limit = limit or 20 + offset = skip while True: - job_page = self._api_client.jobs_get( + jobs_response = self._api_client.jobs_get( limit=current_page_limit, - skip=skip, - pending=pending)["jobs"] - if not job_page: + skip=offset, + pending=pending) + job_page = jobs_response["jobs"] + # count is the total number of jobs that would be returned if + # there was no limit or skip + count = jobs_response["count"] + + job_responses += job_page + + if len(job_responses) == count - skip: # Stop if there are no more jobs returned by the server. break - job_responses += job_page if limit: if len(job_responses) >= limit: # Stop if we have reached the limit. @@ -496,7 +503,7 @@ def jobs( else: current_page_limit = 20 - skip += len(job_page) + offset += len(job_page) return [self._decode_job(job) for job in job_responses] diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index bd2b4852d..bdf4fa0d7 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -301,12 +301,14 @@ def jobs_get(self, limit=None, skip=None, pending=None): limit = limit or len(self._jobs) skip = skip or 0 jobs = list(self._jobs.values()) + count = len(self._jobs) if pending is not None: job_status_list = pending_statuses if pending else returned_statuses jobs = [job for job in jobs if job._status in job_status_list] + count = len(jobs) jobs = jobs[skip:limit+skip] return {"jobs": [job.to_dict() for job in jobs], - "count": len(self._jobs)} + "count": count} def set_program_visibility(self, program_id: str, public: bool) -> None: """Sets a program's visibility. From aea236b0cbf84510ad7ffd8c708b25bb4bf6aa44 Mon Sep 17 00:00:00 2001 From: Jessie Yu Date: Fri, 29 Oct 2021 13:56:22 -0400 Subject: [PATCH 30/48] Allow updating runtime metadata in place (#188) * update runtime metadata * return if no data * fix mypy * Update releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml Co-authored-by: Rathish Cholarajan --- qiskit_ibm/api/clients/runtime.py | 33 +++++---- qiskit_ibm/api/rest/runtime.py | 41 ++++++++--- qiskit_ibm/runtime/ibm_runtime_service.py | 68 +++++++++++++++++-- ...ate-runtime-metadata-d2ddbcfc0d034530.yaml | 9 +++ test/ibm/runtime/fake_runtime_client.py | 35 ++++++++-- test/ibm/runtime/test_runtime.py | 47 +++++++++++++ test/ibm/runtime/test_runtime_integration.py | 27 +++++++- 7 files changed, 220 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 01ff6aff2..330893c2d 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -87,17 +87,6 @@ def program_get(self, program_id: str) -> Dict: """ return self.api.program(program_id).get() - def program_get_data(self, program_id: str) -> Dict: - """Return a specific program and its data. - - Args: - program_id: Program ID. - - Returns: - Program information, including data. - """ - return self.api.program(program_id).get_data() - def set_program_visibility(self, program_id: str, public: bool) -> None: """Sets a program's visibility. @@ -145,14 +134,32 @@ def program_delete(self, program_id: str) -> None: """ self.api.program(program_id).delete() - def program_update(self, program_id: str, program_data: str) -> None: + def program_update( + self, + program_id: str, + program_data: str = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None + ) -> None: """Update a program. Args: program_id: Program ID. program_data: Program data (base64 encoded). + name: Name of the program. + description: Program description. + max_execution_time: Maximum execution time. + spec: Backend requirements, parameters, interim results, return values, etc. """ - self.api.program(program_id).update(program_data) + if program_data: + self.api.program(program_id).update_data(program_data) + + if any([name, description, max_execution_time, spec]): + self.api.program(program_id).update_metadata( + name=name, description=description, + max_execution_time=max_execution_time, spec=spec) def job_get(self, job_id: str) -> Dict: """Get job data. diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 7e774b978..d5aacd673 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -194,15 +194,6 @@ def get(self) -> Dict[str, Any]: url = self.get_url('self') return self.session.get(url).json() - def get_data(self) -> Dict[str, Any]: - """Return program information, including data. - - Returns: - JSON response. - """ - url = self.get_url('data') - return self.session.get(url).json() - def make_public(self) -> None: """Sets a runtime program's visibility to public.""" url = self.get_url('public') @@ -222,8 +213,8 @@ def delete(self) -> None: url = self.get_url('self') self.session.delete(url) - def update(self, program_data: str) -> None: - """Update a program. + def update_data(self, program_data: str) -> None: + """Update program data. Args: program_data: Program data (base64 encoded). @@ -232,6 +223,34 @@ def update(self, program_data: str) -> None: self.session.put(url, data=program_data, headers={'Content-Type': 'application/octet-stream'}) + def update_metadata( + self, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None + ) -> None: + """Update program metadata. + + Args: + name: Name of the program. + description: Program description. + max_execution_time: Maximum execution time. + spec: Backend requirements, parameters, interim results, return values, etc. + """ + url = self.get_url("self") + payload: Dict = {} + if name: + payload["name"] = name + if description: + payload["description"] = description + if max_execution_time: + payload["cost"] = max_execution_time + if spec: + payload["spec"] = spec + + self.session.patch(url, json=payload) + class ProgramJob(RestAdapterBase): """Rest adapter for program job related endpoints.""" diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 55718335c..d5ad9081f 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -16,6 +16,7 @@ from typing import Dict, Callable, Optional, Union, List, Any, Type import json import re +import warnings from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit_ibm import ibm_provider # pylint: disable=unused-import @@ -373,30 +374,83 @@ def _read_metadata( def update_program( self, program_id: str, - data: str, + data: str = None, + metadata: Optional[Union[Dict, str]] = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None ) -> None: """Update a runtime program. + Program metadata can be specified using the `metadata` parameter or + individual parameters, such as `name` and `description`. If the + same metadata field is specified in both places, the individual parameter + takes precedence. + Args: program_id: Program ID. data: Program data or path of the file containing program data to upload. + metadata: Name of the program metadata file or metadata dictionary. + name: New program name. + description: New program description. + max_execution_time: New maximum execution time. + spec: New specifications for backend characteristics, input parameters, + interim results and final result. Raises: RuntimeProgramNotFound: If the program doesn't exist. QiskitRuntimeError: If the request failed. """ - if "def main(" not in data: - # This is the program file - with open(data, "r") as file: - data = file.read() + if not any([data, metadata, name, description, max_execution_time, spec]): + warnings.warn("None of the 'data', 'metadata', 'name', 'description', " + "'max_execution_time', or 'spec' parameters is specified. " + "No update is made.") + return + + if data: + if "def main(" not in data: + # This is the program file + with open(data, "r") as file: + data = file.read() + data = to_base64_string(data) + + if metadata: + metadata = self._read_metadata(metadata=metadata) + combined_metadata = self._merge_metadata( + metadata=metadata, name=name, description=description, + max_execution_time=max_execution_time, spec=spec) + try: - program_data = to_base64_string(data) - self._api_client.program_update(program_id, program_data) + self._api_client.program_update( + program_id, program_data=data, **combined_metadata) except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None raise QiskitRuntimeError(f"Failed to update program: {ex}") from None + def _merge_metadata( + self, + metadata: Optional[Dict] = None, + **kwargs: Any + ) -> Dict: + """Merge multiple copies of metadata. + Args: + metadata: Program metadata. + **kwargs: Additional metadata fields to overwrite. + Returns: + Merged metadata. + """ + merged = {} + metadata = metadata or {} + metadata_keys = ['name', 'max_execution_time', 'description', 'spec'] + for key in metadata_keys: + if kwargs.get(key, None) is not None: + merged[key] = kwargs[key] + elif key in metadata.keys(): + merged[key] = metadata[key] + return merged + def delete_program(self, program_id: str) -> None: """Delete a runtime program. diff --git a/releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml b/releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml new file mode 100644 index 000000000..18ff3d661 --- /dev/null +++ b/releasenotes/notes/update-runtime-metadata-d2ddbcfc0d034530.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + You can now use the :meth:`qiskit_ibm.runtime.IBMRuntimeService.update_program` + method to update the metadata for a Qiskit Runtime program. + Program metadata can be specified using the ``metadata`` parameter or + individual parameters, such as ``name`` and ``description``. If the + same metadata field is specified in both places, the individual parameter + takes precedence. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index bdf4fa0d7..3e28c7de0 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -15,7 +15,8 @@ import time import uuid import json -from typing import Optional +import base64 +from typing import Optional, Dict from concurrent.futures import ThreadPoolExecutor from qiskit_ibm.credentials import Credentials @@ -51,7 +52,7 @@ def to_dict(self, include_data=False): 'creation_date': '2021-09-13T17:27:42Z', 'update_date': '2021-09-14T19:25:32Z'} if include_data: - out['data'] = self._data + out['data'] = base64.standard_b64decode(self._data).decode() out['spec'] = {} if self._backend_requirements: out['spec']['backend_requirements'] = self._backend_requirements @@ -255,15 +256,35 @@ def program_create(self, program_data, name, description, max_execution_time, is_public=is_public) return {'id': program_id} + def program_update( + self, + program_id: str, + program_data: str = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None + ) -> None: + """Update a program.""" + if program_id not in self._programs: + raise RequestsApiError("Program not found", status_code=404) + program = self._programs[program_id] + program._data = program_data or program._data + program._name = name or program._name + program._description = description or program._description + program._cost = max_execution_time or program._cost + if spec: + program._backend_requirements = \ + spec.get("backend_requirements") or program._backend_requirements + program._parameters = spec.get("parameters") or program._parameters + program._return_values = spec.get("return_values") or program._return_values + program._interim_results = spec.get("interim_results") or program._interim_results + def program_get(self, program_id: str): """Return a specific program.""" if program_id not in self._programs: raise RequestsApiError("Program not found", status_code=404) - return self._programs[program_id].to_dict() - - def program_get_data(self, program_id: str): - """Return a specific program and its data.""" - return self._programs[program_id].to_dict(iclude_data=True) + return self._programs[program_id].to_dict(include_data=True) def program_run( self, diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index 954755b43..c7ae84d20 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -340,6 +340,53 @@ def test_upload_program(self): self.assertEqual(max_execution_time, program.max_execution_time) self.assertEqual(program.is_public, is_public) + def test_update_program(self): + """Test updating program.""" + new_data = "def main() {foo=bar}" + new_metadata = copy.deepcopy(self.DEFAULT_METADATA) + new_metadata["name"] = "test_update_program" + new_name = "name2" + new_description = "some other description" + new_cost = self.DEFAULT_METADATA["max_execution_time"] + 100 + new_spec = copy.deepcopy(self.DEFAULT_METADATA["spec"]) + new_spec["backend_requirements"] = {"input_allowed": "runtime"} + + sub_tests = [ + {"data": new_data}, + {"metadata": new_metadata}, + {"data": new_data, "metadata": new_metadata}, + {"metadata": new_metadata, "name": new_name}, + {"data": new_data, "metadata": new_metadata, "description": new_description}, + {"max_execution_time": new_cost, "spec": new_spec} + ] + + for new_vals in sub_tests: + with self.subTest(new_vals=new_vals.keys()): + program_id = self._upload_program() + self.runtime.update_program(program_id=program_id, **new_vals) + updated = self.runtime.program(program_id, refresh=True) + if "data" in new_vals: + raw_program = self.runtime._api_client.program_get(program_id) + self.assertEqual(new_data, raw_program["data"]) + if "metadata" in new_vals and "name" not in new_vals: + self.assertEqual(new_metadata["name"], updated.name) + if "name" in new_vals: + self.assertEqual(new_name, updated.name) + if "description" in new_vals: + self.assertEqual(new_description, updated.description) + if "max_execution_time" in new_vals: + self.assertEqual(new_cost, updated.max_execution_time) + if "spec" in new_vals: + raw_program = self.runtime._api_client.program_get(program_id) + self.assertEqual(new_spec, raw_program["spec"]) + + def test_update_program_no_new_fields(self): + """Test updating a program without any new data.""" + program_id = self._upload_program() + with warnings.catch_warnings(record=True) as warn_cm: + self.runtime.update_program(program_id=program_id) + self.assertEqual(len(warn_cm), 1) + def test_delete_program(self): """Test deleting program.""" program_id = self._upload_program() diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 101440c19..b0d69c01e 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -216,8 +216,8 @@ def test_double_delete_program(self): with self.assertRaises(RuntimeProgramNotFound): self.provider.runtime.delete_program(program_id) - def test_update_program(self): - """Test updating a program.""" + def test_update_program_data(self): + """Test updating program data.""" program_v1 = """ def main(backend, user_messenger, **kwargs): return "version 1" @@ -226,6 +226,7 @@ def main(backend, user_messenger, **kwargs): def main(backend, user_messenger, **kwargs): return "version 2" """ + # TODO retrieve program data instead of run program when #66 is merged program_id = self._upload_program(data=program_v1) job = self._run_program(program_id=program_id) self.assertEqual("version 1", job.result()) @@ -233,6 +234,28 @@ def main(backend, user_messenger, **kwargs): job = self._run_program(program_id=program_id) self.assertEqual("version 2", job.result()) + def test_update_program_metadata(self): + """Test updating program metadata.""" + program_id = self._upload_program() + original = self.provider.runtime.program(program_id) + new_metadata = { + "name": self._get_program_name(), + "description": "test_update_program_metadata", + "max_execution_time": original.max_execution_time + 100, + "spec": { + "return_values": { + "type": "object", + "description": "Some return value" + } + } + } + self.provider.runtime.update_program(program_id=program_id, metadata=new_metadata) + updated = self.provider.runtime.program(program_id, refresh=True) + self.assertEqual(new_metadata["name"], updated.name) + self.assertEqual(new_metadata["description"], updated.description) + self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) + self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) + def test_run_program(self): """Test running a program.""" job = self._run_program(final_result="foo") From f72a108547d265b461076e97ba487412e9efc69d Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Tue, 2 Nov 2021 17:11:32 -0400 Subject: [PATCH 31/48] wip program name query --- qiskit_ibm/api/clients/runtime.py | 4 ++-- qiskit_ibm/api/rest/runtime.py | 5 +++-- qiskit_ibm/runtime/ibm_runtime_service.py | 11 +++++++---- .../notes/query-program-name-823e8e7cfef44f50.yaml | 7 +++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 330893c2d..d1c5399bc 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -39,13 +39,13 @@ def __init__( **credentials.connection_parameters()) self.api = Runtime(self._session) - def list_programs(self) -> Dict[str, Any]: + def list_programs(self, name: str) -> Dict[str, Any]: """Return a list of runtime programs. Returns: A list of quantum programs. """ - return self.api.list_programs() + return self.api.list_programs(name) def program_create( self, diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index d5aacd673..e28738cb1 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -55,14 +55,15 @@ def program_job(self, job_id: str) -> 'ProgramJob': """ return ProgramJob(self.session, job_id) - def list_programs(self) -> Dict[str, Any]: + def list_programs(self, name: str) -> Dict[str, Any]: """Return a list of runtime programs. Returns: JSON response. """ url = self.get_url('programs') - return self.session.get(url).json() + response = self.session.get('{}?name={}'.format(url, name)).json() + return response def create_program( self, diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index b86b01134..d43b957cd 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -108,15 +108,17 @@ def __init__(self, provider: 'ibm_provider.IBMProvider') -> None: self._ws_url = provider.credentials.runtime_url.replace('https', 'wss') self._programs = {} # type: Dict - def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None: + def pprint_programs(self, refresh: bool = False, detailed: bool = False, + name: Optional[str] = "") -> None: """Pretty print information about available runtime programs. Args: 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. + name: Program name. """ - programs = self.programs(refresh) + programs = self.programs(refresh, name) for prog in programs: print("="*50) if detailed: @@ -126,7 +128,7 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None print(f" Name: {prog.name}") print(f" Description: {prog.description}") - def programs(self, refresh: bool = False) -> List[RuntimeProgram]: + def programs(self, refresh: bool = False, name: Optional[str] = "") -> List[RuntimeProgram]: """Return available runtime programs. Currently only program metadata is returned. @@ -134,13 +136,14 @@ def programs(self, refresh: bool = False) -> List[RuntimeProgram]: Args: refresh: If ``True``, re-query the server for the programs. Otherwise return the cached value. + name: Program name. Returns: A list of runtime programs. """ if not self._programs or refresh: self._programs = {} - response = self._api_client.list_programs() + response = self._api_client.list_programs(name) for prog_dict in response.get("programs", []): program = self._to_program(prog_dict) self._programs[program.program_id] = program diff --git a/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml b/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml new file mode 100644 index 000000000..fa646af4d --- /dev/null +++ b/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The ``name`` parameter has been added to + :meth:`qiskit_ibm.runtime.IBMRuntimeService.programs` and + :meth:`qiskit_ibm.runtime.IBMRuntimeService.pprint_programs`. + ``name`` can be used to query by a specific program name. \ No newline at end of file From 4102480cbb5dfd0138de05685f34a74ab708d21a Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Wed, 3 Nov 2021 23:08:22 -0400 Subject: [PATCH 32/48] Allow filtering runtime jobs by program ID (#193) * Allow filtering runtime jobs by program ID * Fix lint --- qiskit_ibm/api/clients/runtime.py | 11 +++++++++-- qiskit_ibm/api/rest/runtime.py | 11 ++++++++++- qiskit_ibm/runtime/ibm_runtime_service.py | 8 +++++--- ...e-filter-jobs-by-program-id-e7ef435bed1081be.yaml | 5 +++++ test/ibm/runtime/fake_runtime_client.py | 5 ++++- test/ibm/runtime/test_runtime.py | 12 ++++++++++++ test/ibm/runtime/test_runtime_integration.py | 9 +++++++++ 7 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/feature-filter-jobs-by-program-id-e7ef435bed1081be.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 330893c2d..c2eaed835 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -174,7 +174,13 @@ def job_get(self, job_id: str) -> Dict: logger.debug("Runtime job get response: %s", response) return response - def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> Dict: + def jobs_get( + self, + limit: int = None, + skip: int = None, + pending: bool = None, + program_id: str = None + ) -> Dict: """Get job data for all jobs. Args: @@ -182,11 +188,12 @@ def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> skip: Number of results to skip. pending: Returns 'QUEUED' and 'RUNNING' jobs if True, returns 'DONE', 'CANCELLED' and 'ERROR' jobs if False. + program_id: Filter by Program ID. Returns: JSON response. """ - return self.api.jobs_get(limit=limit, skip=skip, pending=pending) + return self.api.jobs_get(limit=limit, skip=skip, pending=pending, program_id=program_id) 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 d5aacd673..5340c9298 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -134,7 +134,13 @@ def program_run( data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() - def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> Dict: + def jobs_get( + self, + limit: int = None, + skip: int = None, + pending: bool = None, + program_id: str = None + ) -> Dict: """Get a list of job data. Args: @@ -142,6 +148,7 @@ def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> skip: Number of results to skip. pending: Returns 'QUEUED' and 'RUNNING' jobs if True, returns 'DONE', 'CANCELLED' and 'ERROR' jobs if False. + program_id: Filter by Program ID. Returns: JSON response. @@ -154,6 +161,8 @@ def jobs_get(self, limit: int = None, skip: int = None, pending: bool = None) -> payload['offset'] = skip if pending is not None: payload['pending'] = 'true' if pending else 'false' + if program_id: + payload['program'] = program_id 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 b86b01134..e8a7cee32 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -12,7 +12,6 @@ """Qiskit runtime service.""" -import base64 import logging from typing import Dict, Callable, Optional, Union, List, Any, Type import json @@ -516,7 +515,8 @@ def jobs( self, limit: Optional[int] = 10, skip: int = 0, - pending: bool = None + pending: bool = None, + program_id: str = None ) -> List[RuntimeJob]: """Retrieve all runtime jobs, subject to optional filtering. @@ -526,6 +526,7 @@ def jobs( pending: Filter by job pending state. If ``True``, 'QUEUED' and 'RUNNING' jobs are included. If ``False``, 'DONE', 'CANCELLED' and 'ERROR' jobs are included. + program_id: Filter by Program ID. Returns: A list of runtime jobs. @@ -538,7 +539,8 @@ def jobs( jobs_response = self._api_client.jobs_get( limit=current_page_limit, skip=offset, - pending=pending) + pending=pending, + program_id=program_id) 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/feature-filter-jobs-by-program-id-e7ef435bed1081be.yaml b/releasenotes/notes/feature-filter-jobs-by-program-id-e7ef435bed1081be.yaml new file mode 100644 index 000000000..bac90a5f2 --- /dev/null +++ b/releasenotes/notes/feature-filter-jobs-by-program-id-e7ef435bed1081be.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + You can now pass ``program_id`` parameter to :meth:`qiskit_ibm.runtime.IBMRuntimeService.jobs` + method to filter jobs by Program ID. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 3e28c7de0..a4b4d2607 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -315,7 +315,7 @@ 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): + def jobs_get(self, limit=None, skip=None, pending=None, program_id=None): """Get all jobs.""" pending_statuses = ['QUEUED', 'RUNNING'] returned_statuses = ['COMPLETED', 'FAILED', 'CANCELLED'] @@ -327,6 +327,9 @@ def jobs_get(self, limit=None, skip=None, pending=None): job_status_list = pending_statuses if pending else returned_statuses jobs = [job for job in jobs if job._status in job_status_list] count = len(jobs) + if program_id: + jobs = [job for job in jobs if job._program_id == program_id] + 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 c7ae84d20..1dce869d6 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -601,6 +601,18 @@ def test_jobs_limit_skip_returned(self): rjobs = self.runtime.jobs(limit=limit, skip=skip, pending=False) self.assertEqual(limit, len(rjobs)) + def test_jobs_filter_by_program_id(self): + """Test retrieving jobs by Program ID.""" + program_id = self._upload_program() + program_id_1 = self._upload_program() + job = self._run_program(program_id=program_id) + job_1 = self._run_program(program_id=program_id_1) + job.wait_for_final_state() + job_1.wait_for_final_state() + rjobs = self.runtime.jobs(program_id=program_id) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + def test_cancel_job(self): """Test canceling a job.""" job = self._run_program(job_classes=CancelableRuntimeJob) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index b0d69c01e..568f937a2 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -376,6 +376,15 @@ def test_retrieve_returned_jobs(self): break self.assertTrue(found, f"Returned job {job.job_id} not retrieved.") + def test_retrieve_jobs_by_program_id(self): + """Test retrieving jobs by Program ID.""" + 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) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + def test_cancel_job_queued(self): """Test canceling a queued job.""" _ = self._run_program(iterations=10) From 8e8bcb3932cbabd8e1938fa19de941e7f33707d0 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Thu, 4 Nov 2021 12:19:20 -0400 Subject: [PATCH 33/48] Allow runtime program authors to retrieve program data (#174) * retrieve program data * refetch once if no program data * remove unused import * refresh program on data property * fix lint * Update qiskit_ibm/runtime/runtime_program.py Co-authored-by: Rathish Cholarajan * Update qiskit_ibm/runtime/runtime_program.py Co-authored-by: Rathish Cholarajan * Update qiskit_ibm/runtime/runtime_program.py Co-authored-by: Rathish Cholarajan * add test case * add test case * add default data constant * add _validate_program method * Update test/ibm/runtime/test_runtime.py Co-authored-by: Rathish Cholarajan --- qiskit_ibm/runtime/ibm_runtime_service.py | 4 +- qiskit_ibm/runtime/runtime_program.py | 47 ++++++++++++++++-- ...ogram-data-retrieval-9a9782eb16274593.yaml | 6 +++ test/ibm/runtime/test_runtime.py | 49 ++++++++++++------- test/ibm/runtime/test_runtime_integration.py | 13 +++++ 5 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/program-data-retrieval-9a9782eb16274593.yaml diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index e8a7cee32..c0985517b 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -203,7 +203,9 @@ def _to_program(self, response: Dict) -> RuntimeProgram: creation_date=response.get('creation_date', ""), update_date=response.get('update_date', ""), backend_requirements=backend_requirements, - is_public=response.get('is_public', False)) + is_public=response.get('is_public', False), + data=response.get('data', ""), + api_client=self._api_client) def run( self, diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index 5e648244e..77b5cf08b 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -16,8 +16,8 @@ import re from typing import Optional, List, Dict from types import SimpleNamespace -from qiskit_ibm.exceptions import IBMInputValueError - +from qiskit_ibm.exceptions import IBMInputValueError, IBMNotAuthorizedError +from ..api.clients.runtime import RuntimeClient logger = logging.getLogger(__name__) @@ -55,7 +55,9 @@ def __init__( backend_requirements: Optional[Dict] = None, creation_date: str = "", update_date: str = "", - is_public: Optional[bool] = False + is_public: Optional[bool] = False, + data: str = "", + api_client: Optional[RuntimeClient] = None ) -> None: """RuntimeProgram constructor. @@ -71,6 +73,8 @@ def __init__( creation_date: Program creation date. update_date: Program last updated date. is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. + data: Program data. + api_client: Runtime api client. """ self._name = program_name self._id = program_id @@ -83,6 +87,8 @@ def __init__( self._creation_date = creation_date self._update_date = update_date self._is_public = is_public + self._data = data + self._api_client = api_client def __str__(self) -> str: def _format_common(schema: Dict) -> None: @@ -258,6 +264,41 @@ def is_public(self) -> bool: """ return self._is_public + @property + def data(self) -> str: + """Program data. + + Returns: + Program data. + + Raises: + IBMNotAuthorizedError: if user is not the program author. + """ + if not self._data: + response = self._api_client.program_get(self._id) + self._backend_requirements = {} + self._parameters = {} + self._return_values = {} + self._interim_results = {} + if "spec" in response: + self._backend_requirements = response["spec"].get('backend_requirements', {}) + self._parameters = response["spec"].get('parameters', {}) + self._return_values = response["spec"].get('return_values', {}) + self._interim_results = response["spec"].get('interim_results', {}) + self._name = response['name'] + self._id = response['id'] + self._description = response.get('description', "") + self._max_execution_time = response.get('cost', 0) + self._creation_date = response.get('creation_date', "") + self._update_date = response.get('update_date', "") + self._is_public = response.get('is_public', False) + if 'data' in response: + self._data = response['data'] + else: + raise IBMNotAuthorizedError( + 'Only program authors are authorized to retrieve program data') + return self._data + class ParameterNamespace(SimpleNamespace): """ A namespace for program parameters with validation. diff --git a/releasenotes/notes/program-data-retrieval-9a9782eb16274593.yaml b/releasenotes/notes/program-data-retrieval-9a9782eb16274593.yaml new file mode 100644 index 000000000..e1daea8fa --- /dev/null +++ b/releasenotes/notes/program-data-retrieval-9a9782eb16274593.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + If you are the author of a runtime program, + you can now use :attr:`qiskit_ibm.runtime.RuntimeProgram.data` + property to retrieve the program data as a string. diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index 1dce869d6..ac2e0a174 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -67,6 +67,7 @@ class TestRuntime(IBMTestCase): """Class for testing runtime modules.""" + DEFAULT_DATA = "def main() {}" DEFAULT_METADATA = { "name": "qiskit-test", "description": "Test program.", @@ -427,10 +428,18 @@ def test_run_program_with_custom_runtime_image(self): self.assertTrue(job.result()) self.assertEqual(job.image, image) + def test_retrieve_program_data(self): + """Test retrieving program data""" + program_id = self._upload_program(name="qiskit-test") + self.runtime.programs() + program = self.runtime.program(program_id) + self.assertEqual(program.data, self.DEFAULT_DATA) + self._validate_program(program) + def test_program_params_validation(self): """Test program parameters validation process""" program_id = self.runtime.upload_program( - data="def main() {}", metadata=self.DEFAULT_METADATA) + data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA) program = self.runtime.program(program_id) params: ParameterNamespace = program.parameters() params.param1 = 'Hello, World' @@ -448,7 +457,7 @@ def test_program_params_validation(self): def test_program_params_namespace(self): """Test running a program using parameter namespace.""" program_id = self.runtime.upload_program( - data="def main() {}", metadata=self.DEFAULT_METADATA) + data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA) params = self.runtime.program(program_id).parameters() params.param1 = "Hello World" self._run_program(program_id, inputs=params) @@ -686,23 +695,10 @@ def test_program_metadata(self): for metadata in sub_tests: with self.subTest(metadata_type=type(metadata)): - program_id = self.runtime.upload_program(data="def main() {}", metadata=metadata) + program_id = self.runtime.upload_program(data=self.DEFAULT_DATA, metadata=metadata) program = self.runtime.program(program_id) self.runtime.delete_program(program_id) - self.assertEqual(self.DEFAULT_METADATA['name'], program.name) - self.assertEqual(self.DEFAULT_METADATA['description'], program.description) - self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], - program.max_execution_time) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - self.assertEqual(self.DEFAULT_METADATA['spec']['backend_requirements'], - program.backend_requirements) - self.assertEqual(self.DEFAULT_METADATA['spec']['parameters'], - program.parameters().metadata) - self.assertEqual(self.DEFAULT_METADATA['spec']['return_values'], - program.return_values) - self.assertEqual(self.DEFAULT_METADATA['spec']['interim_results'], - program.interim_results) + self._validate_program(program) def test_different_providers(self): """Test retrieving job submitted with different provider.""" @@ -718,7 +714,7 @@ def _upload_program(self, name=None, max_execution_time=300, is_public: bool = False): """Upload a new program.""" name = name or uuid.uuid4().hex - data = "def main() {}" + data = self.DEFAULT_DATA metadata = copy.deepcopy(self.DEFAULT_METADATA) metadata.update(name=name) metadata.update(is_public=is_public) @@ -762,3 +758,20 @@ def _populate_jobs_with_all_statuses(self, jobs, program_id): jobs.append(self._run_program(program_id, final_status='CANCELLED')) returned_jobs_count += 1 return (jobs, pending_jobs_count, returned_jobs_count) + + def _validate_program(self, program): + """Validate a program.""" + self.assertEqual(self.DEFAULT_METADATA['name'], program.name) + self.assertEqual(self.DEFAULT_METADATA['description'], program.description) + self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], + program.max_execution_time) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) + self.assertEqual(self.DEFAULT_METADATA['spec']['backend_requirements'], + program.backend_requirements) + self.assertEqual(self.DEFAULT_METADATA['spec']['parameters'], + program.parameters().metadata) + self.assertEqual(self.DEFAULT_METADATA['spec']['return_values'], + program.return_values) + self.assertEqual(self.DEFAULT_METADATA['spec']['interim_results'], + program.interim_results) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 568f937a2..5d6abc3c9 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -147,6 +147,19 @@ def test_list_program(self): self.assertEqual(self.program_id, program.program_id) self._validate_program(program) + def test_retrieve_program_data(self): + """Test retrieving program data""" + program = self.provider.runtime.program(self.program_id) + self.assertEqual(self.RUNTIME_PROGRAM, program.data) + self._validate_program(program) + + def test_retrieve_unauthorized_program_data(self): + """Test retrieving program data when user is not the program author""" + program = self.provider.runtime.program('sample-program') + self._validate_program(program) + with self.assertRaises(IBMNotAuthorizedError): + return program.data + def test_upload_program(self): """Test uploading a program.""" max_execution_time = 3000 From 90484ccacc8d82251fe4e0d7c1307e6067f506d3 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Thu, 4 Nov 2021 16:13:14 -0400 Subject: [PATCH 34/48] add test case, refactor logic --- qiskit_ibm/api/rest/runtime.py | 5 ++++- qiskit_ibm/runtime/ibm_runtime_service.py | 2 +- test/ibm/runtime/fake_runtime_client.py | 7 +++++-- test/ibm/runtime/test_runtime.py | 10 ++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 92629c3d1..8b7e73668 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -62,7 +62,10 @@ def list_programs(self, name: str) -> Dict[str, Any]: JSON response. """ url = self.get_url('programs') - response = self.session.get('{}?name={}'.format(url, name)).json() + payload: Dict[str, Union[int, str]] = {} + if name: + payload['name'] = name + response = self.session.get(url, params=payload).json() return response def create_program( diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 78a35143a..8345d4e91 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -140,7 +140,7 @@ def programs(self, refresh: bool = False, name: Optional[str] = "") -> List[Runt Returns: A list of runtime programs. """ - if not self._programs or refresh: + if not self._programs or refresh or name: self._programs = {} response = self._api_client.list_programs(name) for prog_dict in response.get("programs", []): diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index a4b4d2607..4d4b31763 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -232,11 +232,14 @@ 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): + def list_programs(self, name): """List all programs.""" programs = [] for prog in self._programs.values(): - programs.append(prog.to_dict()) + if not name: + programs.append(prog.to_dict()) + if name == prog.to_dict()['name']: + programs.append(prog.to_dict()) return {"programs": programs} def program_create(self, program_data, name, description, max_execution_time, diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index ac2e0a174..8bd6cfec5 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -302,6 +302,16 @@ def test_list_programs(self): all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) + def test_list_programs_with_name(self): + """Test listing programs with the name parameter""" + program_id = self._upload_program(name="sample-program") + programs = self.runtime.programs(name="sample-program") + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_id, all_ids) + programs = self.runtime.programs(name="qiskit-test") + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_id, all_ids) + def test_list_program(self): """Test listing a single program.""" program_id = self._upload_program() From 07f0d53b5eefd711b6db94d2c1d4ca5488e50955 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Thu, 4 Nov 2021 21:34:33 -0400 Subject: [PATCH 35/48] Update cache after updating program (#196) --- qiskit_ibm/runtime/ibm_runtime_service.py | 8 +++ qiskit_ibm/runtime/runtime_program.py | 55 +++++++++++++------- test/ibm/runtime/test_runtime_integration.py | 7 +-- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index c0985517b..4db69021f 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -431,6 +431,10 @@ def update_program( raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None raise QiskitRuntimeError(f"Failed to update program: {ex}") from None + if program_id in self._programs: + program = self._programs[program_id] + program._refresh() + def _merge_metadata( self, metadata: Optional[Dict] = None, @@ -492,6 +496,10 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: raise RuntimeJobNotFound(f"Program not found: {ex.message}") from None raise QiskitRuntimeError(f"Failed to set program visibility: {ex}") from None + if program_id in self._programs: + program = self._programs[program_id] + program._is_public = public + def job(self, job_id: str) -> RuntimeJob: """Retrieve a runtime job. diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py index 77b5cf08b..7cd1b6a2e 100644 --- a/qiskit_ibm/runtime/runtime_program.py +++ b/qiskit_ibm/runtime/runtime_program.py @@ -17,7 +17,9 @@ from typing import Optional, List, Dict from types import SimpleNamespace from qiskit_ibm.exceptions import IBMInputValueError, IBMNotAuthorizedError +from .exceptions import QiskitRuntimeError, RuntimeProgramNotFound from ..api.clients.runtime import RuntimeClient +from ..api.exceptions import RequestsApiError logger = logging.getLogger(__name__) @@ -275,30 +277,43 @@ def data(self) -> str: IBMNotAuthorizedError: if user is not the program author. """ if not self._data: - response = self._api_client.program_get(self._id) - self._backend_requirements = {} - self._parameters = {} - self._return_values = {} - self._interim_results = {} - if "spec" in response: - self._backend_requirements = response["spec"].get('backend_requirements', {}) - self._parameters = response["spec"].get('parameters', {}) - self._return_values = response["spec"].get('return_values', {}) - self._interim_results = response["spec"].get('interim_results', {}) - self._name = response['name'] - self._id = response['id'] - self._description = response.get('description', "") - self._max_execution_time = response.get('cost', 0) - self._creation_date = response.get('creation_date', "") - self._update_date = response.get('update_date', "") - self._is_public = response.get('is_public', False) - if 'data' in response: - self._data = response['data'] - else: + self._refresh() + if not self._data: raise IBMNotAuthorizedError( 'Only program authors are authorized to retrieve program data') return self._data + def _refresh(self) -> None: + """Refresh program data and metadata + + Raises: + RuntimeProgramNotFound: If the program does not exist. + QiskitRuntimeError: If the request failed. + """ + try: + response = self._api_client.program_get(self._id) + except RequestsApiError as ex: + if ex.status_code == 404: + raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise QiskitRuntimeError(f"Failed to get program: {ex}") from None + self._backend_requirements = {} + self._parameters = {} + self._return_values = {} + self._interim_results = {} + if "spec" in response: + self._backend_requirements = response["spec"].get('backend_requirements', {}) + self._parameters = response["spec"].get('parameters', {}) + self._return_values = response["spec"].get('return_values', {}) + self._interim_results = response["spec"].get('interim_results', {}) + self._name = response['name'] + self._id = response['id'] + self._description = response.get('description', "") + self._max_execution_time = response.get('cost', 0) + self._creation_date = response.get('creation_date', "") + self._update_date = response.get('update_date', "") + self._is_public = response.get('is_public', False) + self._data = response.get('data', "") + class ParameterNamespace(SimpleNamespace): """ A namespace for program parameters with validation. diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 5d6abc3c9..9aab64d9c 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -239,13 +239,10 @@ def main(backend, user_messenger, **kwargs): def main(backend, user_messenger, **kwargs): return "version 2" """ - # TODO retrieve program data instead of run program when #66 is merged program_id = self._upload_program(data=program_v1) - job = self._run_program(program_id=program_id) - self.assertEqual("version 1", job.result()) + self.assertEqual(program_v1, self.provider.runtime.program(program_id).data) self.provider.runtime.update_program(program_id=program_id, data=program_v2) - job = self._run_program(program_id=program_id) - self.assertEqual("version 2", job.result()) + self.assertEqual(program_v2, self.provider.runtime.program(program_id).data) def test_update_program_metadata(self): """Test updating program metadata.""" From 91e4ba8f68126cd3359da9895e950ad41d4f2f66 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 02:48:24 -0400 Subject: [PATCH 36/48] add integration test --- test/ibm/runtime/test_runtime.py | 2 +- test/ibm/runtime/test_runtime_integration.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index 8bd6cfec5..4abb90075 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -302,7 +302,7 @@ def test_list_programs(self): all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) - def test_list_programs_with_name(self): + def test_list_programs_with_program_name(self): """Test listing programs with the name parameter""" program_id = self._upload_program(name="sample-program") programs = self.runtime.programs(name="sample-program") diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 9aab64d9c..e3a5dac12 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -141,6 +141,16 @@ def test_list_programs(self): found = True self.assertTrue(found, f"Program {self.program_id} not found!") + def test_list_programs_with_program_name(self): + """Test listing programs with program name.""" + program_id = self._upload_program(name="sample-program") + programs = self.provider.runtime.programs(name="sample-program") + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_id, all_ids) + programs = self.provider.runtime.programs(name="qiskit-test") + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_id, all_ids) + def test_list_program(self): """Test listing a single program.""" program = self.provider.runtime.program(self.program_id) From b458efc5f2c2b0198eaa38fa97c9d2e5e045a2c5 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 09:42:20 -0400 Subject: [PATCH 37/48] update doc strings --- qiskit_ibm/api/clients/runtime.py | 3 +++ qiskit_ibm/api/rest/runtime.py | 5 ++++- qiskit_ibm/runtime/ibm_runtime_service.py | 4 ++-- releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml | 3 ++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index e66aabaea..cf2248009 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -42,6 +42,9 @@ def __init__( def list_programs(self, name: str) -> Dict[str, Any]: """Return a list of runtime programs. + Args: + name: Name of the program. + Returns: A list of quantum programs. """ diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 8b7e73668..524200d51 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -58,11 +58,14 @@ def program_job(self, job_id: str) -> 'ProgramJob': def list_programs(self, name: str) -> Dict[str, Any]: """Return a list of runtime programs. + Args: + name: Name of the program. + Returns: JSON response. """ url = self.get_url('programs') - payload: Dict[str, Union[int, str]] = {} + payload: Dict[str, Union[str]] = {} if name: payload['name'] = name response = self.session.get(url, params=payload).json() diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index 4af7a3e65..a558d0f12 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -115,7 +115,7 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False, 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. - name: Program name. + name: Only retrieve programs with the exact program name given. """ programs = self.programs(refresh, name) for prog in programs: @@ -135,7 +135,7 @@ def programs(self, refresh: bool = False, name: Optional[str] = "") -> List[Runt Args: refresh: If ``True``, re-query the server for the programs. Otherwise return the cached value. - name: Program name. + name: Only retrieve programs with the exact program name given. Returns: A list of runtime programs. diff --git a/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml b/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml index fa646af4d..4bdca5c5c 100644 --- a/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml +++ b/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml @@ -4,4 +4,5 @@ upgrade: The ``name`` parameter has been added to :meth:`qiskit_ibm.runtime.IBMRuntimeService.programs` and :meth:`qiskit_ibm.runtime.IBMRuntimeService.pprint_programs`. - ``name`` can be used to query by a specific program name. \ No newline at end of file + ``name`` can be used to query by a specific program name. The + ``name`` given must be an exact match with an actual program name. \ No newline at end of file From 4b48ec05fc59302918fe458eb59f8662c07fd55f Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:14:05 -0400 Subject: [PATCH 38/48] Update releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml Co-authored-by: Rathish Cholarajan --- releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml b/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml index 4bdca5c5c..5c4a475a6 100644 --- a/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml +++ b/releasenotes/notes/query-program-name-823e8e7cfef44f50.yaml @@ -3,6 +3,6 @@ upgrade: - | The ``name`` parameter has been added to :meth:`qiskit_ibm.runtime.IBMRuntimeService.programs` and - :meth:`qiskit_ibm.runtime.IBMRuntimeService.pprint_programs`. - ``name`` can be used to query by a specific program name. The + :meth:`qiskit_ibm.runtime.IBMRuntimeService.pprint_programs` + which can be used to filter by a specific program name. The ``name`` given must be an exact match with an actual program name. \ No newline at end of file From 87a5429981bf57bd501ec613a197e5cf2c264b92 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:27:11 -0400 Subject: [PATCH 39/48] Update test/ibm/runtime/test_runtime.py Co-authored-by: Rathish Cholarajan --- test/ibm/runtime/test_runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index 4abb90075..af5fe8744 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -302,7 +302,7 @@ def test_list_programs(self): all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) - def test_list_programs_with_program_name(self): + def test_filter_programs_with_program_name(self): """Test listing programs with the name parameter""" program_id = self._upload_program(name="sample-program") programs = self.runtime.programs(name="sample-program") From 1d6e8ff47e59c074eef4f9c95e8296b7f404b352 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:27:26 -0400 Subject: [PATCH 40/48] Update test/ibm/runtime/test_runtime.py Co-authored-by: Rathish Cholarajan --- test/ibm/runtime/test_runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index af5fe8744..cae4038fd 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -303,7 +303,7 @@ def test_list_programs(self): self.assertIn(program_id, all_ids) def test_filter_programs_with_program_name(self): - """Test listing programs with the name parameter""" + """Test filter programs with the program name""" program_id = self._upload_program(name="sample-program") programs = self.runtime.programs(name="sample-program") all_ids = [prog.program_id for prog in programs] From 005a5d425f828026bb771a0a2f94b3e03302430b Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:27:37 -0400 Subject: [PATCH 41/48] Update test/ibm/runtime/test_runtime_integration.py Co-authored-by: Rathish Cholarajan --- test/ibm/runtime/test_runtime_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index e3a5dac12..2e3837766 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -141,7 +141,7 @@ def test_list_programs(self): found = True self.assertTrue(found, f"Program {self.program_id} not found!") - def test_list_programs_with_program_name(self): + def test_filter_programs_with_program_name(self): """Test listing programs with program name.""" program_id = self._upload_program(name="sample-program") programs = self.provider.runtime.programs(name="sample-program") From 15655ec9f424cf2b0b6fe263fe14f26825d9b888 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:27:47 -0400 Subject: [PATCH 42/48] Update test/ibm/runtime/test_runtime_integration.py Co-authored-by: Rathish Cholarajan --- test/ibm/runtime/test_runtime_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 2e3837766..54adb6e46 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -142,7 +142,7 @@ def test_list_programs(self): self.assertTrue(found, f"Program {self.program_id} not found!") def test_filter_programs_with_program_name(self): - """Test listing programs with program name.""" + """Test filter programs with program name.""" program_id = self._upload_program(name="sample-program") programs = self.provider.runtime.programs(name="sample-program") all_ids = [prog.program_id for prog in programs] From 35a98798c2ef0425a4b33cbf507a77f836a3c5a7 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:29:36 -0400 Subject: [PATCH 43/48] Update test/ibm/runtime/test_runtime_integration.py Co-authored-by: Rathish Cholarajan --- test/ibm/runtime/test_runtime_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 54adb6e46..8cfa60636 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -143,8 +143,8 @@ def test_list_programs(self): def test_filter_programs_with_program_name(self): """Test filter programs with program name.""" - program_id = self._upload_program(name="sample-program") - programs = self.provider.runtime.programs(name="sample-program") + program_id = self._upload_program(name="qiskit-test-sample") + programs = self.provider.runtime.programs(name="qiskit-test-sample") all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) programs = self.provider.runtime.programs(name="qiskit-test") From 8b01221ebcf3a938f5c92da47a81e00bcf3ed75d Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 5 Nov 2021 12:29:42 -0400 Subject: [PATCH 44/48] Update test/ibm/runtime/test_runtime.py Co-authored-by: Rathish Cholarajan --- test/ibm/runtime/test_runtime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index cae4038fd..25a2fcd80 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -304,8 +304,8 @@ def test_list_programs(self): def test_filter_programs_with_program_name(self): """Test filter programs with the program name""" - program_id = self._upload_program(name="sample-program") - programs = self.runtime.programs(name="sample-program") + program_id = self._upload_program(name="qiskit-test-sample") + programs = self.runtime.programs(name="qiskit-test-sample") all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) programs = self.runtime.programs(name="qiskit-test") From 813307631c8143d49f9447b13e809439c31e4df7 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Sun, 7 Nov 2021 19:57:54 -0500 Subject: [PATCH 45/48] Allow filtering runtime jobs by provider (#197) * add provider param * split provider into hub/group/project * add reno * wip add test case * fix lint/docs * Update qiskit_ibm/runtime/ibm_runtime_service.py Co-authored-by: Rathish Cholarajan * Update releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml Co-authored-by: Rathish Cholarajan * refactor test cases * remove print * add integration test Co-authored-by: Rathish Cholarajan --- qiskit_ibm/api/clients/runtime.py | 11 ++++++-- qiskit_ibm/api/rest/runtime.py | 10 ++++++- qiskit_ibm/runtime/ibm_runtime_service.py | 22 ++++++++++++++-- ...ter-jobs-by-provider-dead04faaf223840.yaml | 5 ++++ test/ibm/runtime/fake_runtime_client.py | 26 ++++++++++++++++--- test/ibm/runtime/test_runtime.py | 20 +++++++++++++- test/ibm/runtime/test_runtime_integration.py | 16 ++++++++++++ 7 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/filter-jobs-by-provider-dead04faaf223840.yaml 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) From 7f474e985e0cea79b3b51ceb64c22413117a89c1 Mon Sep 17 00:00:00 2001 From: Rathish Cholarajan Date: Sun, 7 Nov 2021 21:36:45 -0500 Subject: [PATCH 46/48] Update qiskit_ibm/api/rest/runtime.py --- qiskit_ibm/api/rest/runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index fea35a28d..bc5da83d5 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -65,7 +65,7 @@ def list_programs(self, name: str) -> Dict[str, Any]: JSON response. """ url = self.get_url('programs') - payload: Dict[str, Union[str]] = {} + payload: Dict[str, str] = {} if name: payload['name'] = name response = self.session.get(url, params=payload).json() From 4522d9b4788b3e5798dcb758afd45477ea4729c1 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Mon, 8 Nov 2021 01:56:09 -0500 Subject: [PATCH 47/48] Support pagination for retrieving runtime programs (#170) * wip add limit/offset params * add reno * refactor & update test case * offset -> skip, implement refresh logic * refresh when skip/limit not default * Update releasenotes/notes/runtime-program-pagination-8d599ae984a5ce33.yaml Co-authored-by: Rathish Cholarajan * Update releasenotes/notes/runtime-program-pagination-8d599ae984a5ce33.yaml Co-authored-by: Rathish Cholarajan * add to test case * refactor refresh logic * refactor * fix lint * add integration test * update doc string * Update qiskit_ibm/api/clients/runtime.py Co-authored-by: Rathish Cholarajan * cleanup merge * Apply suggestions from code review * fix lint refactor * Fetch all programs upfront 20 at a time and store in cache For subsequent requests paginate and return from cache * Fix integration test Co-authored-by: Rathish Cholarajan Co-authored-by: Rathish Cholarajan --- qiskit_ibm/api/clients/runtime.py | 10 +++-- qiskit_ibm/api/rest/runtime.py | 15 ++++++-- qiskit_ibm/runtime/ibm_runtime_service.py | 38 +++++++++++++++---- ...e-program-pagination-8d599ae984a5ce33.yaml | 9 +++++ test/ibm/runtime/fake_runtime_client.py | 4 +- test/ibm/runtime/test_runtime.py | 14 +++++++ test/ibm/runtime/test_runtime_integration.py | 15 ++++++++ 7 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/runtime-program-pagination-8d599ae984a5ce33.yaml diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py index 38ef6fea2..5cc4df6b5 100644 --- a/qiskit_ibm/api/clients/runtime.py +++ b/qiskit_ibm/api/clients/runtime.py @@ -39,13 +39,17 @@ def __init__( **credentials.connection_parameters()) self.api = Runtime(self._session) - def list_programs(self) -> Dict[str, Any]: + def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: """Return a list of runtime programs. + Args: + limit: The number of programs to return. + skip: The number of programs to skip. + Returns: - A list of quantum programs. + A list of runtime programs. """ - return self.api.list_programs() + return self.api.list_programs(limit, skip) def program_create( self, diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py index 2a68744cb..e44265c96 100644 --- a/qiskit_ibm/api/rest/runtime.py +++ b/qiskit_ibm/api/rest/runtime.py @@ -55,14 +55,23 @@ def program_job(self, job_id: str) -> 'ProgramJob': """ return ProgramJob(self.session, job_id) - def list_programs(self) -> Dict[str, Any]: + def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: """Return a list of runtime programs. + Args: + limit: The number of programs to return. + skip: The number of programs to skip. + Returns: - JSON response. + A list of runtime programs. """ url = self.get_url('programs') - return self.session.get(url).json() + payload: Dict[str, int] = {} + if limit: + payload['limit'] = limit + if skip: + payload['offset'] = skip + return self.session.get(url, params=payload).json() def create_program( self, diff --git a/qiskit_ibm/runtime/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py index e6eda3d57..a131b4298 100644 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ b/qiskit_ibm/runtime/ibm_runtime_service.py @@ -107,15 +107,19 @@ def __init__(self, provider: 'ibm_provider.IBMProvider') -> None: self._ws_url = provider.credentials.runtime_url.replace('https', 'wss') self._programs = {} # type: Dict - def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None: + def pprint_programs(self, refresh: bool = False, detailed: bool = False, + limit: int = 20, skip: int = 0) -> None: """Pretty print information about available runtime programs. Args: 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. + 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) + programs = self.programs(refresh, limit, skip) for prog in programs: print("="*50) if detailed: @@ -125,7 +129,8 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False) -> None print(f" Name: {prog.name}") print(f" Description: {prog.description}") - def programs(self, refresh: bool = False) -> List[RuntimeProgram]: + def programs(self, refresh: bool = False, + limit: int = 20, skip: int = 0) -> List[RuntimeProgram]: """Return available runtime programs. Currently only program metadata is returned. @@ -133,17 +138,34 @@ def programs(self, refresh: bool = False) -> List[RuntimeProgram]: Args: refresh: If ``True``, re-query the server for the programs. Otherwise return the cached value. + 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. """ + if skip is None: + skip = 0 if not self._programs or refresh: self._programs = {} - response = self._api_client.list_programs() - for prog_dict in response.get("programs", []): - program = self._to_program(prog_dict) - self._programs[program.program_id] = program - return list(self._programs.values()) + 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 limit is None: + limit = len(self._programs) + return list(self._programs.values())[skip:limit+skip] def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: """Retrieve a runtime program. diff --git a/releasenotes/notes/runtime-program-pagination-8d599ae984a5ce33.yaml b/releasenotes/notes/runtime-program-pagination-8d599ae984a5ce33.yaml new file mode 100644 index 000000000..2e3d3644d --- /dev/null +++ b/releasenotes/notes/runtime-program-pagination-8d599ae984a5ce33.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + ``limit`` and ``skip`` parameters have been added to + :meth:`qiskit_ibm.runtime.IBMRuntimeService.programs` and + :meth:`qiskit_ibm.runtime.IBMRuntimeService.pprint_programs`. + ``limit`` can be used to set the number of runtime programs returned + and ``skip`` is the number of programs to skip when retrieving + programs. diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py index 3a4ffd6eb..a8b6643fd 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/ibm/runtime/fake_runtime_client.py @@ -242,12 +242,12 @@ 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): + def list_programs(self, limit, skip): """List all programs.""" programs = [] for prog in self._programs.values(): programs.append(prog.to_dict()) - return {"programs": programs} + return {"programs": programs[skip:limit+skip], "count": len(self._programs)} def program_create(self, program_data, name, description, max_execution_time, spec=None, is_public=False): diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index 810d1a5b7..1964ddbd1 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -302,6 +302,20 @@ def test_list_programs(self): all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) + def test_list_programs_with_limit_skip(self): + """Test listing programs with limit and skip.""" + program_1 = self._upload_program() + program_2 = self._upload_program() + program_3 = self._upload_program() + programs = self.runtime.programs(limit=2, skip=1) + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_1, all_ids) + self.assertIn(program_2, all_ids) + self.assertIn(program_3, all_ids) + programs = self.runtime.programs(limit=3) + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_1, all_ids) + def test_list_program(self): """Test listing a single program.""" program_id = self._upload_program() diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py index 40dd08c76..418be70e4 100644 --- a/test/ibm/runtime/test_runtime_integration.py +++ b/test/ibm/runtime/test_runtime_integration.py @@ -141,6 +141,21 @@ def test_list_programs(self): found = True self.assertTrue(found, f"Program {self.program_id} not found!") + def test_list_programs_with_limit_skip(self): + """Test listing programs with limit and skip.""" + self._upload_program() + self._upload_program() + self._upload_program() + programs = self.provider.runtime.programs(limit=3) + all_ids = [prog.program_id for prog in programs] + self.assertEqual(len(all_ids), 3) + programs = self.provider.runtime.programs(limit=2, skip=1) + some_ids = [prog.program_id for prog in programs] + self.assertEqual(len(some_ids), 2) + self.assertNotIn(all_ids[0], some_ids) + self.assertIn(all_ids[1], some_ids) + self.assertIn(all_ids[2], some_ids) + def test_list_program(self): """Test listing a single program.""" program = self.provider.runtime.program(self.program_id) From d35b56f7b7ea638d98f1c23f85ea3cbec8a5b4de Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Mon, 8 Nov 2021 15:21:26 -0500 Subject: [PATCH 48/48] update program name --- test/ibm/runtime/test_runtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py index f68d6d76e..0addf0e40 100644 --- a/test/ibm/runtime/test_runtime.py +++ b/test/ibm/runtime/test_runtime.py @@ -308,7 +308,7 @@ def test_filter_programs_with_program_name(self): programs = self.runtime.programs(name="qiskit-test-sample") all_ids = [prog.program_id for prog in programs] self.assertIn(program_id, all_ids) - programs = self.runtime.programs(name="fafewfe") + programs = self.runtime.programs(name="qiskit-test") all_ids = [prog.program_id for prog in programs] self.assertNotIn(program_id, all_ids)