From e3f08f46b453ffd8d74daf51da78e248aaefb701 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 20:41:21 -0700 Subject: [PATCH 01/13] New: support engagement against a specified quantum processor endpoint --- pyquil/api/_engagement_manager.py | 12 +++++++++--- pyquil/api/_qpu.py | 9 +++++---- pyquil/api/_qpu_client.py | 5 ++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 9e6b2ea8a..96ff3c3ba 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -47,11 +47,15 @@ def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: self._cached_engagements: Dict[str, EngagementWithCredentials] = {} self._lock = threading.Lock() - def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = 10.0) -> EngagementWithCredentials: + def get_engagement( + self, *, quantum_processor_id: str, request_timeout: float = 10.0, endpoint_id: Optional[str] = None + ) -> EngagementWithCredentials: """ Gets an engagement for the given quantum processor. If an engagement was already fetched previously and remains valid, it will be returned instead of creating a new engagement. + :param endpoint_id: Optional ID of the endpoint to use for engagement. If provided, it must + correspond to an endpoint serving the provided Quantum Processor. :param quantum_processor_id: Quantum processor being engaged. :param request_timeout: Timeout for request, in seconds. :return: Fetched or cached engagement. @@ -61,8 +65,10 @@ def get_engagement(self, *, quantum_processor_id: str, request_timeout: float = with qcs_client( client_configuration=self._client_configuration, request_timeout=request_timeout ) as client: # type: httpx.Client - request = CreateEngagementRequest(quantum_processor_id=quantum_processor_id) - self._cached_engagements[quantum_processor_id] = create_engagement( + request = CreateEngagementRequest( + quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id + ) + self._cached_engagements[(quantum_processor_id, endpoint_id)] = create_engagement( client=client, json_body=request ).parsed return self._cached_engagements[quantum_processor_id] diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 76a98ed74..a9bdeaf8b 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -115,6 +115,7 @@ def __init__( timeout: float = 10.0, client_configuration: Optional[QCSClientConfiguration] = None, engagement_manager: Optional[EngagementManager] = None, + endpoint_id: Optional[str] = None, ) -> None: """ A connection to the QPU. @@ -123,10 +124,9 @@ def __init__( :param priority: The priority with which to insert jobs into the QPU queue. Lower integers correspond to higher priority. :param timeout: Time limit for requests, in seconds. - :param client_configuration: Optional client configuration. If none is provided, a default - one will be loaded. - :param engagement_manager: Optional engagement manager. If none is provided, a default one - will be created. + :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. + :param endpoint_id: Optional endpoint ID to be used for engagement. + :param engagement_manager: Optional engagement manager. If none is provided, a default one will be created. """ super().__init__() @@ -136,6 +136,7 @@ def __init__( engagement_manager = engagement_manager or EngagementManager(client_configuration=client_configuration) self._qpu_client = QPUClient( quantum_processor_id=quantum_processor_id, + endpoint_id=endpoint_id, engagement_manager=engagement_manager, request_timeout=timeout, ) diff --git a/pyquil/api/_qpu_client.py b/pyquil/api/_qpu_client.py index c4d56864e..e1cd7e81a 100644 --- a/pyquil/api/_qpu_client.py +++ b/pyquil/api/_qpu_client.py @@ -15,7 +15,7 @@ ############################################################################## from dataclasses import dataclass from datetime import datetime -from typing import Dict, cast, Tuple, Union, List, Any +from typing import Dict, Optional, cast, Tuple, Union, List, Any import rpcq from dateutil.parser import parse as parsedate @@ -103,6 +103,7 @@ def __init__( self, *, quantum_processor_id: str, + endpoint_id: Optional[str] = None, engagement_manager: EngagementManager, request_timeout: float = 10.0, ) -> None: @@ -114,6 +115,7 @@ def __init__( :param request_timeout: Timeout for requests, in seconds. """ self.quantum_processor_id = quantum_processor_id + self._endpoint_id = endpoint_id self._engagement_manager = engagement_manager self.timeout = request_timeout @@ -157,6 +159,7 @@ def get_buffers(self, request: GetBuffersRequest) -> GetBuffersResponse: @retry(exceptions=TimeoutError, tries=2) # type: ignore def _rpcq_request(self, method_name: str, *args: Any, **kwargs: Any) -> Any: engagement = self._engagement_manager.get_engagement( + endpoint_id=self._endpoint_id, quantum_processor_id=self.quantum_processor_id, request_timeout=self.timeout, ) From b0742c308128df459edbd0d7e3d75843a20527a3 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 22:39:21 -0700 Subject: [PATCH 02/13] No code: add test case --- test/unit/test_engagement_manager.py | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/unit/test_engagement_manager.py b/test/unit/test_engagement_manager.py index 2c0c2c81f..abb10398c 100644 --- a/test/unit/test_engagement_manager.py +++ b/test/unit/test_engagement_manager.py @@ -60,6 +60,45 @@ def test_get_engagement__reuses_engagement_when_cached_engagement_unexpired( assert engagement is cached_engagement +def test_get_engagement__using_endpoint_id(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): + """ + Assert that endpoint ID is correctly used to engage against an endpoint. + """ + engagement_manager = EngagementManager(client_configuration=client_configuration) + + engagement = unexpired_engagement(quantum_processor_id="some-processor") + + endpoint_engagement = unexpired_engagement(quantum_processor_id="some-processor") + endpoint_engagement.endpoint_id = "test-endpoint" + + httpx_mock.add_response( + method="POST", + url=f"{client_configuration.profile.api_url}/v1/engagements", + match_content=json.dumps( + { + "endpointId": endpoint_engagement.endpoint_id, + "quantumProcessorId": endpoint_engagement.quantum_processor_id, + } + ).encode(), + json=endpoint_engagement.to_dict(), + ) + + httpx_mock.add_response( + method="POST", + url=f"{client_configuration.profile.api_url}/v1/engagements", + match_content=json.dumps({"quantumProcessorId": engagement.quantum_processor_id}).encode(), + json=engagement.to_dict(), + ) + + fetched_quantum_processor_engagement = engagement_manager.get_engagement(quantum_processor_id="some-processor") + fetched_endpoint_engagement = engagement_manager.get_engagement( + quantum_processor_id="some-processor", endpoint_id="test-endpoint" + ) + + assert engagement == fetched_quantum_processor_engagement + assert endpoint_engagement == fetched_endpoint_engagement + + def cache_engagement( engagement_manager: EngagementManager, engagement: EngagementWithCredentials, From cc890bed25b118d7c11495a642ae88e4a6f98f4f Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 22:39:38 -0700 Subject: [PATCH 03/13] No code: update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1f4dea3..75bd767ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Changelog ### Improvements and Changes +- `get_qc` and `QPU` now both accept an `endpoint_id` argument which is used to engage + against a specific QCS [quantum processor endpoint](https://docs.api.qcs.rigetti.com/#tag/endpoints). + ### Bugfixes - Allow `np.ndarray` when writing QAM memory. Disallow non-integer and non-float types. From f934c4c6880fa5d86461aa6522bac2a1c1c3141d Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 22:44:48 -0700 Subject: [PATCH 04/13] Fix: type check --- pyquil/api/_engagement_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 96ff3c3ba..0303f1872 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -15,13 +15,14 @@ ############################################################################## import threading from datetime import datetime -from typing import Dict, Optional, TYPE_CHECKING +from typing import Dict, Optional, Tuple, TYPE_CHECKING from dateutil.parser import parse as parsedate from dateutil.tz import tzutc from qcs_api_client.client import QCSClientConfiguration from qcs_api_client.models import EngagementWithCredentials, CreateEngagementRequest from qcs_api_client.operations.sync import create_engagement +from qcs_api_client.types import Unset from pyquil.api._qcs_client import qcs_client @@ -44,7 +45,7 @@ def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: :param client_configuration: Client configuration, used for refreshing engagements. """ self._client_configuration = client_configuration - self._cached_engagements: Dict[str, EngagementWithCredentials] = {} + self._cached_engagements: Dict[Tuple[str, Optional[str]], EngagementWithCredentials] = {} self._lock = threading.Lock() def get_engagement( @@ -66,7 +67,7 @@ def get_engagement( client_configuration=self._client_configuration, request_timeout=request_timeout ) as client: # type: httpx.Client request = CreateEngagementRequest( - quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id + quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id or Unset() ) self._cached_engagements[(quantum_processor_id, endpoint_id)] = create_engagement( client=client, json_body=request From ffd262b465a60345b77085d50295c4f0aba0879f Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 15 Jun 2021 22:48:13 -0700 Subject: [PATCH 05/13] No code: fix test --- test/unit/test_qpu_client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/unit/test_qpu_client.py b/test/unit/test_qpu_client.py index ada3725f8..5f9c86244 100644 --- a/test/unit/test_qpu_client.py +++ b/test/unit/test_qpu_client.py @@ -14,9 +14,11 @@ # limitations under the License. ############################################################################## from datetime import datetime, timedelta +from typing import Union from unittest import mock import pytest +from qcs_api_client.types import UNSET import rpcq from dateutil.tz import tzutc from pytest_mock import MockerFixture @@ -132,8 +134,14 @@ def test_fetches_engagement_for_quantum_processor_on_request( engagement_manager=mock_engagement_manager, request_timeout=3.14, ) - mock_engagement_manager.get_engagement.return_value = engagement( - quantum_processor_id=processor_id, + + def mock_get_engagement( + quantum_processor_id: str, endpoint_id: Union[str, UNSET], request_timeout: float + ) -> EngagementWithCredentials: + assert quantum_processor_id == processor_id + assert request_timeout == qpu_client.timeout + return engagement( + quantum_processor_id="some-processor", seconds_left=9999, credentials=engagement_credentials, port=1234, From 95c1f2e83a42f31776eae494a97397ed70603eec Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 10:00:39 -0700 Subject: [PATCH 06/13] Fix: pass endpoint_id through get_qc --- pyquil/api/_quantum_computer.py | 5 +++++ test/unit/test_quantum_computer.py | 36 +++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 29b5d58cc..c9e31b588 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -725,6 +725,7 @@ def get_qc( compiler_timeout: float = 10.0, execution_timeout: float = 10.0, client_configuration: Optional[QCSClientConfiguration] = None, + endpoint_id: Optional[str] = None, engagement_manager: Optional[EngagementManager] = None, ) -> QuantumComputer: """ @@ -794,9 +795,12 @@ def get_qc( :param compiler_timeout: Time limit for compilation requests, in seconds. :param execution_timeout: Time limit for execution requests, in seconds. :param client_configuration: Optional client configuration. If none is provided, a default one will be loaded. + :param endpoint_id: Optional quantum processor endpoint ID, as used in the `QCS API Docs`_. :param engagement_manager: Optional engagement manager. If none is provided, a default one will be created. :return: A pre-configured QuantumComputer + + .. _QCS API Docs: https://docs.api.qcs.rigetti.com/#tag/endpoints """ client_configuration = client_configuration or QCSClientConfiguration.load() @@ -862,6 +866,7 @@ def get_qc( quantum_processor_id=quantum_processor.quantum_processor_id, timeout=execution_timeout, client_configuration=client_configuration, + endpoint_id=endpoint_id, engagement_manager=engagement_manager, ) compiler = QPUCompiler( diff --git a/test/unit/test_quantum_computer.py b/test/unit/test_quantum_computer.py index f1c7dbff1..08fdcd933 100644 --- a/test/unit/test_quantum_computer.py +++ b/test/unit/test_quantum_computer.py @@ -30,6 +30,8 @@ from pyquil.pyqvm import PyQVM from pyquil.quantum_processor import NxQuantumProcessor from pyquil.quilbase import Declare, MemoryReference +from pytest_httpx import HTTPXMock +from qcs_api_client.models.instruction_set_architecture import InstructionSetArchitecture from rpcq.messages import ParameterAref @@ -202,7 +204,7 @@ def test_run(client_configuration: QCSClientConfiguration): MEASURE(2, MemoryReference("ro", 2)), ).wrap_in_numshots_loop(1000) ) - bitstrings = result.readout_data.get('ro') + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 3) parity = np.sum(bitstrings, axis=1) % 3 @@ -221,7 +223,7 @@ def test_run_pyqvm_noiseless(client_configuration: QCSClientConfiguration): for q in range(3): prog += MEASURE(q, ro[q]) result = qc.run(prog.wrap_in_numshots_loop(1000)) - bitstrings = result.readout_data.get('ro') + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 3) parity = np.sum(bitstrings, axis=1) % 3 @@ -240,7 +242,7 @@ def test_run_pyqvm_noisy(client_configuration: QCSClientConfiguration): for q in range(3): prog += MEASURE(q, ro[q]) result = qc.run(prog.wrap_in_numshots_loop(1000)) - bitstrings = result.readout_data.get('ro') + bitstrings = result.readout_data.get("ro") assert bitstrings.shape == (1000, 3) parity = np.sum(bitstrings, axis=1) % 3 @@ -266,7 +268,7 @@ def test_readout_symmetrization(client_configuration: QCSClientConfiguration): prog.wrap_in_numshots_loop(1000) result_1 = qc.run(prog) - bitstrings_1 = result_1.readout_data.get('ro') + bitstrings_1 = result_1.readout_data.get("ro") avg0_us = np.mean(bitstrings_1[:, 0]) avg1_us = 1 - np.mean(bitstrings_1[:, 1]) diff_us = avg1_us - avg0_us @@ -422,7 +424,7 @@ def test_qc_run(client_configuration: QCSClientConfiguration): MEASURE(0, ("ro", 0)), ).wrap_in_numshots_loop(3) ) - ).readout_data.get('ro') + ).readout_data.get("ro") assert bs.shape == (3, 1) @@ -472,7 +474,7 @@ def test_run_with_parameters(client_configuration: QCSClientConfiguration, param ).wrap_in_numshots_loop(1000) executable.write_memory(region_name="theta", value=param) - bitstrings = qc.run(executable).readout_data.get('ro') + bitstrings = qc.run(executable).readout_data.get("ro") assert bitstrings.shape == (1000, 1) assert all([bit == 1 for bit in bitstrings]) @@ -556,7 +558,7 @@ def test_get_qvm_with_topology_2(client_configuration: QCSClientConfiguration): MEASURE(7, ("ro", 2)), ).wrap_in_numshots_loop(5) ) - ).readout_data.get('ro') + ).readout_data.get("ro") assert results.shape == (5, 3) assert all(r[0] == 1 for r in results) @@ -575,7 +577,7 @@ def test_noisy(client_configuration: QCSClientConfiguration): MEASURE(0, ("ro", 0)), ).wrap_in_numshots_loop(10000) qc = get_qc("1q-qvm", noisy=True, client_configuration=client_configuration) - result = qc.run(qc.compile(p)).readout_data.get('ro') + result = qc.run(qc.compile(p)).readout_data.get("ro") assert result.mean() < 1.0 @@ -831,3 +833,21 @@ def test_qc_expectation_on_qvm(client_configuration: QCSClientConfiguration, dum assert np.isclose(results[2][0].expectation, 1.0, atol=0.01) assert np.isclose(results[2][0].std_err, 0) assert results[2][0].total_counts == 20000 + + +def test_get_qc_endpoint_id( + client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock, qcs_aspen8_isa: InstructionSetArchitecture +): + """ + Assert that get_qc passes a specified ``endpoint_id`` through to its QPU when constructed + for a live quantum processor. + """ + httpx_mock.add_response( + method="GET", + url=f"{client_configuration.profile.api_url}/v1/quantumProcessors/test/instructionSetArchitecture", + json=qcs_aspen8_isa.to_dict(), + ) + + qc = get_qc("test", endpoint_id="test-endpoint") + + assert qc.qam._qpu_client._endpoint_id == "test-endpoint" From fbfc0aa2bc2b38c24626b204c340ff69c4659ef9 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Wed, 16 Jun 2021 13:57:30 -0700 Subject: [PATCH 07/13] Fix: use correct UNSET singleton --- pyquil/api/_engagement_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 0303f1872..2d70e3fc2 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -22,7 +22,7 @@ from qcs_api_client.client import QCSClientConfiguration from qcs_api_client.models import EngagementWithCredentials, CreateEngagementRequest from qcs_api_client.operations.sync import create_engagement -from qcs_api_client.types import Unset +from qcs_api_client.types import UNSET from pyquil.api._qcs_client import qcs_client @@ -67,7 +67,7 @@ def get_engagement( client_configuration=self._client_configuration, request_timeout=request_timeout ) as client: # type: httpx.Client request = CreateEngagementRequest( - quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id or Unset() + quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id or UNSET ) self._cached_engagements[(quantum_processor_id, endpoint_id)] = create_engagement( client=client, json_body=request From 7120664a60043bc5253032cf3d70674b6fa5fcbe Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:45:38 -0700 Subject: [PATCH 08/13] No code: Replace pytest-httpx with respx --- poetry.lock | 36 ++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index 366b3f9d2..0c1874881 100644 --- a/poetry.lock +++ b/poetry.lock @@ -903,21 +903,6 @@ python-versions = "*" freezegun = ">0.3" pytest = ">=3.0.0" -[[package]] -name = "pytest-httpx" -version = "0.9.0" -description = "Send responses to httpx." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -httpx = ">=0.15.0,<0.16.0" -pytest = ">=6.0.0,<7.0.0" - -[package.extras] -testing = ["pytest-asyncio (>=0.14.0,<0.15.0)", "pytest-cov (>=2.0.0,<3.0.0)"] - [[package]] name = "pytest-mock" version = "3.6.1" @@ -1076,6 +1061,17 @@ urllib3 = ">=1.21.1,<1.27" security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[[package]] +name = "respx" +version = "0.15.1" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +httpx = ">=0.15" + [[package]] name = "retry" version = "0.9.2" @@ -1415,7 +1411,7 @@ latex = ["ipython"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c059d449facf631a118d653669a4f0891da7d2a38e5402232bd5310045997975" +content-hash = "050a9333fe1cc8e432323d9a4ca369d5b984da16a5b286a63d5ae10445cff067" [metadata.files] alabaster = [ @@ -1912,10 +1908,6 @@ pytest-freezegun = [ {file = "pytest-freezegun-0.4.2.zip", hash = "sha256:19c82d5633751bf3ec92caa481fb5cffaac1787bd485f0df6436fd6242176949"}, {file = "pytest_freezegun-0.4.2-py2.py3-none-any.whl", hash = "sha256:5318a6bfb8ba4b709c8471c94d0033113877b3ee02da5bfcd917c1889cde99a7"}, ] -pytest-httpx = [ - {file = "pytest_httpx-0.9.0-py3-none-any.whl", hash = "sha256:f337cc19176600ed0615c5ec8390646306857d35ee49e5b662e68dfc0fc17dce"}, - {file = "pytest_httpx-0.9.0.tar.gz", hash = "sha256:7bfcc9344e13f441068181fb909fc86a545f25b4343ad98de55c1327ab336bfd"}, -] pytest-mock = [ {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, @@ -2072,6 +2064,10 @@ requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] +respx = [ + {file = "respx-0.15.1-py2.py3-none-any.whl", hash = "sha256:07b69af4f127e6651ab0fd104a484bcb9d98b901b25234d4158851ff5a37e34a"}, + {file = "respx-0.15.1.tar.gz", hash = "sha256:d3438b7ec2edb5a4f575c0ca5a51c37b9a55c42c6693d178a1917aeca290de7f"}, +] retry = [ {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, diff --git a/pyproject.toml b/pyproject.toml index 90fa3b279..b86dc74cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,13 +45,13 @@ black = "^20.8b1" flake8 = "^3.8.1" pytest = "^6.2.2" pytest-cov = "^2.11.1" -pytest-httpx = "^0.9" mypy = "0.740" pytest-xdist = "^2.2.1" pytest-rerunfailures = "^9.1.1" pytest-timeout = "^1.4.2" pytest-mock = "^3.6.1" pytest-freezegun = "^0.4.2" +respx = "^0.15" [tool.poetry.extras] latex = ["ipython"] From a6daf6f5670a41f8f524a6799018df75843ebed8 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:46:43 -0700 Subject: [PATCH 09/13] No code: update tests to use respx --- pyquil/api/_engagement_manager.py | 9 +- test/unit/test_engagement_manager.py | 147 ++++++++++++++++----------- test/unit/test_qpu_client.py | 13 ++- test/unit/test_quantum_computer.py | 14 ++- test/unit/test_qvm_client.py | 114 +++++++++------------ 5 files changed, 156 insertions(+), 141 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 2d70e3fc2..9efe277a3 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -52,8 +52,9 @@ def get_engagement( self, *, quantum_processor_id: str, request_timeout: float = 10.0, endpoint_id: Optional[str] = None ) -> EngagementWithCredentials: """ - Gets an engagement for the given quantum processor. If an engagement was already fetched previously and - remains valid, it will be returned instead of creating a new engagement. + Gets an engagement for the given quantum processor endpoint. + If an engagement was already fetched previously and remains valid, it will be returned instead + of creating a new engagement. :param endpoint_id: Optional ID of the endpoint to use for engagement. If provided, it must correspond to an endpoint serving the provided Quantum Processor. @@ -62,7 +63,7 @@ def get_engagement( :return: Fetched or cached engagement. """ with self._lock: - if not self._engagement_valid(self._cached_engagements.get(quantum_processor_id)): + if not self._engagement_valid(self._cached_engagements.get((quantum_processor_id, endpoint_id))): with qcs_client( client_configuration=self._client_configuration, request_timeout=request_timeout ) as client: # type: httpx.Client @@ -72,7 +73,7 @@ def get_engagement( self._cached_engagements[(quantum_processor_id, endpoint_id)] = create_engagement( client=client, json_body=request ).parsed - return self._cached_engagements[quantum_processor_id] + return self._cached_engagements[(quantum_processor_id, endpoint_id)] @staticmethod def _engagement_valid(engagement: Optional[EngagementWithCredentials]) -> bool: diff --git a/test/unit/test_engagement_manager.py b/test/unit/test_engagement_manager.py index abb10398c..63c0b1ac8 100644 --- a/test/unit/test_engagement_manager.py +++ b/test/unit/test_engagement_manager.py @@ -14,111 +14,138 @@ # limitations under the License. ############################################################################## import json +from typing import List, Optional -from pytest_httpx import HTTPXMock -from qcs_api_client.models import EngagementWithCredentials, EngagementCredentials - -from pyquil.api._engagement_manager import EngagementManager +import httpx +import respx from pyquil.api import QCSClientConfiguration +from pyquil.api._engagement_manager import EngagementManager +from qcs_api_client.models import EngagementCredentials, EngagementWithCredentials +DEFAULT_ENDPOINT_ID = "some-endpoint" +@respx.mock def test_get_engagement__refreshes_engagement_when_cached_engagement_expired( - client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock + client_configuration: QCSClientConfiguration, ): engagement_manager = EngagementManager(client_configuration=client_configuration) cache_engagement( - engagement_manager, expired_engagement(quantum_processor_id="some-processor"), client_configuration, httpx_mock + engagement_manager, expired_engagement(quantum_processor_id="some-processor"), client_configuration, ) - httpx_mock.add_response( - method="POST", + respx.post( url=f"{client_configuration.profile.api_url}/v1/engagements", - match_content=json.dumps({"quantumProcessorId": "some-processor"}).encode(), - json=unexpired_engagement(quantum_processor_id="some-processor").to_dict(), + json={"quantumProcessorId": "some-processor"}, + ).respond(json=unexpired_engagement(quantum_processor_id="some-processor").to_dict()) + + engagement = engagement_manager.get_engagement(quantum_processor_id="some-processor") + + assert engagement == unexpired_engagement(quantum_processor_id="some-processor") + + +@respx.mock +def test_get_engagement__refreshes_engagement_when_cached_engagement_expired__using_endpoint_id( + client_configuration: QCSClientConfiguration, +): + """ + Assert that endpoint ID is correctly used to engage against an endpoint when the cached engagement has expired. + """ + engagement_manager = EngagementManager(client_configuration=client_configuration) + cache_engagement( + engagement_manager, + expired_engagement(quantum_processor_id="some-processor", endpoint_id="custom-endpoint"), + client_configuration, ) + respx.post( + url=f"{client_configuration.profile.api_url}/v1/engagements", + json={"quantumProcessorId": "some-processor", "endpointId": "custom-endpoint"}, + ).respond(json=unexpired_engagement(quantum_processor_id="some-processor").to_dict()) engagement = engagement_manager.get_engagement(quantum_processor_id="some-processor") assert engagement == unexpired_engagement(quantum_processor_id="some-processor") +@respx.mock def test_get_engagement__reuses_engagement_when_cached_engagement_unexpired( - client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock + client_configuration: QCSClientConfiguration, ): engagement_manager = EngagementManager(client_configuration=client_configuration) cached_engagement = cache_engagement( engagement_manager, unexpired_engagement(quantum_processor_id="some-processor"), client_configuration, - httpx_mock, ) - network_calls_before = len(httpx_mock.get_requests()) + network_calls_before = respx.calls.call_count engagement = engagement_manager.get_engagement(quantum_processor_id="some-processor") - network_calls_after = len(httpx_mock.get_requests()) + network_calls_after = respx.calls.call_count assert network_calls_before == network_calls_after assert engagement is cached_engagement -def test_get_engagement__using_endpoint_id(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): +@respx.mock +def test_get_engagement__reuses_engagement_when_cached_engagement_unexpired__using_endpoint_id( + client_configuration: QCSClientConfiguration, +): """ Assert that endpoint ID is correctly used to engage against an endpoint. """ engagement_manager = EngagementManager(client_configuration=client_configuration) - - engagement = unexpired_engagement(quantum_processor_id="some-processor") - - endpoint_engagement = unexpired_engagement(quantum_processor_id="some-processor") - endpoint_engagement.endpoint_id = "test-endpoint" - - httpx_mock.add_response( - method="POST", - url=f"{client_configuration.profile.api_url}/v1/engagements", - match_content=json.dumps( - { - "endpointId": endpoint_engagement.endpoint_id, - "quantumProcessorId": endpoint_engagement.quantum_processor_id, - } - ).encode(), - json=endpoint_engagement.to_dict(), - ) - - httpx_mock.add_response( - method="POST", - url=f"{client_configuration.profile.api_url}/v1/engagements", - match_content=json.dumps({"quantumProcessorId": engagement.quantum_processor_id}).encode(), - json=engagement.to_dict(), + cached_engagement = cache_engagement( + engagement_manager, + unexpired_engagement(quantum_processor_id="some-processor", endpoint_id="custom-endpoint"), + client_configuration, ) + network_calls_before = respx.calls.call_count - fetched_quantum_processor_engagement = engagement_manager.get_engagement(quantum_processor_id="some-processor") - fetched_endpoint_engagement = engagement_manager.get_engagement( - quantum_processor_id="some-processor", endpoint_id="test-endpoint" - ) + engagement = engagement_manager.get_engagement(quantum_processor_id="some-processor", endpoint_id="custom-endpoint") + network_calls_after = respx.calls.call_count - assert engagement == fetched_quantum_processor_engagement - assert endpoint_engagement == fetched_endpoint_engagement + assert network_calls_before == network_calls_after + assert engagement is cached_engagement def cache_engagement( engagement_manager: EngagementManager, engagement: EngagementWithCredentials, client_configuration: QCSClientConfiguration, - httpx_mock: HTTPXMock, ) -> EngagementWithCredentials: - httpx_mock.add_response( - method="POST", - url=f"{client_configuration.profile.api_url}/v1/engagements", - match_content=json.dumps({"quantumProcessorId": engagement.quantum_processor_id}).encode(), - json=engagement.to_dict(), - ) + mock_engagement(client_configuration=client_configuration, engagement=engagement) - cached_engagement = engagement_manager.get_engagement(quantum_processor_id=engagement.quantum_processor_id) + if engagement.endpoint_id == DEFAULT_ENDPOINT_ID: + endpoint_id = None + else: + endpoint_id = engagement.endpoint_id + + cached_engagement = engagement_manager.get_engagement( + quantum_processor_id=engagement.quantum_processor_id, endpoint_id=endpoint_id + ) assert cached_engagement == engagement return cached_engagement -def make_engagement(*, quantum_processor_id: str, expires_at: str) -> EngagementWithCredentials: +def mock_engagement(engagement: EngagementWithCredentials, *, client_configuration: QCSClientConfiguration): + """ + Apply and respond with an engagement when it matches. + """ + + if engagement.endpoint_id == DEFAULT_ENDPOINT_ID: + respx.post( + url=f"{client_configuration.profile.api_url}/v1/engagements", + json={"quantumProcessorId": engagement.quantum_processor_id} + ).respond(json=engagement.to_dict()) + + respx.post( + url=f"{client_configuration.profile.api_url}/v1/engagements", + json={"endpointId": engagement.endpoint_id} + ).respond(json=engagement.to_dict()) + + +def make_engagement( + *, quantum_processor_id: str, endpoint_id: Optional[str] = None, expires_at: str +) -> EngagementWithCredentials: return EngagementWithCredentials( address="tcp://example.com/qpu", credentials=EngagementCredentials( @@ -126,7 +153,7 @@ def make_engagement(*, quantum_processor_id: str, expires_at: str) -> Engagement client_secret="client-secret-123", server_public="server-public-123", ), - endpoint_id="some-endpoint", + endpoint_id=endpoint_id or DEFAULT_ENDPOINT_ID, expires_at=expires_at, quantum_processor_id=quantum_processor_id, user_id="some-user", @@ -134,9 +161,13 @@ def make_engagement(*, quantum_processor_id: str, expires_at: str) -> Engagement ) -def unexpired_engagement(*, quantum_processor_id: str) -> EngagementWithCredentials: - return make_engagement(quantum_processor_id=quantum_processor_id, expires_at="9999-01-01T00:00:00Z") +def unexpired_engagement(*, quantum_processor_id: str, endpoint_id: Optional[str] = None) -> EngagementWithCredentials: + return make_engagement( + quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id, expires_at="9999-01-01T00:00:00Z" + ) -def expired_engagement(*, quantum_processor_id: str) -> EngagementWithCredentials: - return make_engagement(quantum_processor_id=quantum_processor_id, expires_at="1970-01-01T00:00:00Z") +def expired_engagement(*, quantum_processor_id: str, endpoint_id: Optional[str] = None) -> EngagementWithCredentials: + return make_engagement( + quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id, expires_at="1970-01-01T00:00:00Z" + ) diff --git a/test/unit/test_qpu_client.py b/test/unit/test_qpu_client.py index 5f9c86244..704256f2b 100644 --- a/test/unit/test_qpu_client.py +++ b/test/unit/test_qpu_client.py @@ -14,11 +14,11 @@ # limitations under the License. ############################################################################## from datetime import datetime, timedelta -from typing import Union +from typing import Optional, Union from unittest import mock import pytest -from qcs_api_client.types import UNSET +from qcs_api_client.types import UNSET, Unset import rpcq from dateutil.tz import tzutc from pytest_mock import MockerFixture @@ -136,7 +136,7 @@ def test_fetches_engagement_for_quantum_processor_on_request( ) def mock_get_engagement( - quantum_processor_id: str, endpoint_id: Union[str, UNSET], request_timeout: float + quantum_processor_id: str, request_timeout: float = 10.0, endpoint_id: Optional[str] = None ) -> EngagementWithCredentials: assert quantum_processor_id == processor_id assert request_timeout == qpu_client.timeout @@ -147,6 +147,8 @@ def mock_get_engagement( port=1234, ) + mock_engagement_manager.get_engagement.side_effect = mock_get_engagement + patch_rpcq_client(mocker=mocker, return_value="") request = RunProgramRequest( @@ -157,6 +159,7 @@ def mock_get_engagement( ) qpu_client.run_program(request) mock_engagement_manager.get_engagement.assert_called_once_with( + endpoint_id=None, quantum_processor_id=processor_id, request_timeout=qpu_client.timeout, ) @@ -254,8 +257,8 @@ def test_run_program__retries_on_timeout( # ASSERT # Engagement should be fetched twice, once per RPC call mock_engagement_manager.get_engagement.assert_has_calls([ - mocker.call(quantum_processor_id='some-processor', request_timeout=1.0), - mocker.call(quantum_processor_id='some-processor', request_timeout=1.0), + mocker.call(quantum_processor_id='some-processor', request_timeout=1.0, endpoint_id=None), + mocker.call(quantum_processor_id='some-processor', request_timeout=1.0, endpoint_id=None), ]) # RPC call should happen twice since the first one times out qpu_request = QPURequest(**request_kwargs) # Thing QPUClient gives to rpcq.Client diff --git a/test/unit/test_quantum_computer.py b/test/unit/test_quantum_computer.py index 08fdcd933..fd8207f51 100644 --- a/test/unit/test_quantum_computer.py +++ b/test/unit/test_quantum_computer.py @@ -5,6 +5,8 @@ import networkx as nx import numpy as np import pytest +import respx + from pyquil import Program, list_quantum_computers from pyquil.api import QCSClientConfiguration from pyquil.api._quantum_computer import ( @@ -30,7 +32,6 @@ from pyquil.pyqvm import PyQVM from pyquil.quantum_processor import NxQuantumProcessor from pyquil.quilbase import Declare, MemoryReference -from pytest_httpx import HTTPXMock from qcs_api_client.models.instruction_set_architecture import InstructionSetArchitecture from rpcq.messages import ParameterAref @@ -835,18 +836,15 @@ def test_qc_expectation_on_qvm(client_configuration: QCSClientConfiguration, dum assert results[2][0].total_counts == 20000 -def test_get_qc_endpoint_id( - client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock, qcs_aspen8_isa: InstructionSetArchitecture -): +@respx.mock +def test_get_qc_endpoint_id(client_configuration: QCSClientConfiguration, qcs_aspen8_isa: InstructionSetArchitecture): """ Assert that get_qc passes a specified ``endpoint_id`` through to its QPU when constructed for a live quantum processor. """ - httpx_mock.add_response( - method="GET", + respx.get( url=f"{client_configuration.profile.api_url}/v1/quantumProcessors/test/instructionSetArchitecture", - json=qcs_aspen8_isa.to_dict(), - ) + ).respond(json=qcs_aspen8_isa.to_dict()) qc = get_qc("test", endpoint_id="test-endpoint") diff --git a/test/unit/test_qvm_client.py b/test/unit/test_qvm_client.py index 57fe1270b..5254a54ce 100644 --- a/test/unit/test_qvm_client.py +++ b/test/unit/test_qvm_client.py @@ -14,11 +14,10 @@ # limitations under the License. ############################################################################## import json -import time from typing import Any, Dict import httpx -from pytest_httpx import HTTPXMock, to_response +import respx from qcs_api_client.client import QCSClientConfiguration from pyquil.api._qvm_client import ( @@ -41,48 +40,34 @@ def test_init__sets_base_url_and_timeout(client_configuration: QCSClientConfigur assert qvm_client.timeout == 3.14 -def test_sets_timeout_on_requests(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): - qvm_client = QVMClient(client_configuration=client_configuration, request_timeout=0.1) - - def assert_timeout(request: httpx.Request, ext: Dict[str, Any]): - assert ext["timeout"] == httpx.Timeout(qvm_client.timeout).as_dict() - return to_response(data="1.2.3 [abc123]") - - httpx_mock.add_callback(assert_timeout) - - qvm_client.get_version() - - -def test_get_version__returns_version(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): +@respx.mock +def test_get_version__returns_version(client_configuration: QCSClientConfiguration): qvm_client = QVMClient(client_configuration=client_configuration) - httpx_mock.add_response( + respx.post( url=client_configuration.profile.applications.pyquil.qvm_url, - match_content=json.dumps({"type": "version"}).encode(), - data="1.2.3 [abc123]", - ) + json={"type": "version"}, + ).respond(status_code=200, text="1.2.3 [abc123]") assert qvm_client.get_version() == "1.2.3" -def test_run_program__returns_results(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): +@respx.mock +def test_run_program__returns_results(client_configuration: QCSClientConfiguration): qvm_client = QVMClient(client_configuration=client_configuration) - httpx_mock.add_response( + respx.post( url=client_configuration.profile.applications.pyquil.qvm_url, - match_content=json.dumps( - { - "type": "multishot", - "compiled-quil": "some-program", - "addresses": {"ro": True}, - "trials": 1, - "measurement-noise": (3.14, 1.61, 6.28), - "gate-noise": (1.0, 2.0, 3.0), - "rng-seed": 314, - }, - ).encode(), - json={"ro": [[1, 0, 1]]}, - ) + json={ + "type": "multishot", + "compiled-quil": "some-program", + "addresses": {"ro": True}, + "trials": 1, + "measurement-noise": (3.14, 1.61, 6.28), + "gate-noise": (1.0, 2.0, 3.0), + "rng-seed": 314, + }, + ).respond(status_code=200, json={"ro": [[1, 0, 1]]}) request = RunProgramRequest( program="some-program", @@ -95,24 +80,22 @@ def test_run_program__returns_results(client_configuration: QCSClientConfigurati assert qvm_client.run_program(request) == RunProgramResponse(results={"ro": [[1, 0, 1]]}) -def test_run_and_measure_program__returns_results(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): +@respx.mock +def test_run_and_measure_program__returns_results(client_configuration: QCSClientConfiguration): qvm_client = QVMClient(client_configuration=client_configuration) - httpx_mock.add_response( + respx.post( url=client_configuration.profile.applications.pyquil.qvm_url, - match_content=json.dumps( - { - "type": "multishot-measure", - "compiled-quil": "some-program", - "qubits": [0, 1, 2], - "trials": 1, - "measurement-noise": (3.14, 1.61, 6.28), - "gate-noise": (1.0, 2.0, 3.0), - "rng-seed": 314, - }, - ).encode(), - json=[[1, 0, 1]], - ) + json={ + "type": "multishot-measure", + "compiled-quil": "some-program", + "qubits": [0, 1, 2], + "trials": 1, + "measurement-noise": (3.14, 1.61, 6.28), + "gate-noise": (1.0, 2.0, 3.0), + "rng-seed": 314, + }, + ).respond(status_code=200, json=[[1, 0, 1]]) request = RunAndMeasureProgramRequest( program="some-program", @@ -125,19 +108,20 @@ def test_run_and_measure_program__returns_results(client_configuration: QCSClien assert qvm_client.run_and_measure_program(request) == RunAndMeasureProgramResponse(results=[[1, 0, 1]]) -def test_measure_expectation__returns_expectation(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): +@respx.mock +def test_measure_expectation__returns_expectation(client_configuration: QCSClientConfiguration): qvm_client = QVMClient(client_configuration=client_configuration) - httpx_mock.add_response( + respx.post( url=client_configuration.profile.applications.pyquil.qvm_url, - match_content=json.dumps( - { - "type": "expectation", - "state-preparation": "some-program", - "operators": ["some-op-program"], - "rng-seed": 314, - }, - ).encode(), + json={ + "type": "expectation", + "state-preparation": "some-program", + "operators": ["some-op-program"], + "rng-seed": 314, + }, + ).respond( + status_code=200, json=[0.161], ) @@ -149,22 +133,20 @@ def test_measure_expectation__returns_expectation(client_configuration: QCSClien assert qvm_client.measure_expectation(request) == MeasureExpectationResponse(expectations=[0.161]) -def test_get_wavefunction__returns_wavefunction(client_configuration: QCSClientConfiguration, httpx_mock: HTTPXMock): +@respx.mock +def test_get_wavefunction__returns_wavefunction(client_configuration: QCSClientConfiguration): qvm_client = QVMClient(client_configuration=client_configuration) - httpx_mock.add_response( + respx.post( url=client_configuration.profile.applications.pyquil.qvm_url, - match_content=json.dumps( - { + json={ "type": "wavefunction", "compiled-quil": "some-program", "measurement-noise": (3.14, 1.61, 6.28), "gate-noise": (1.0, 2.0, 3.0), "rng-seed": 314, }, - ).encode(), - data=b"some-wavefunction", - ) + ).respond(status_code=200, text="some-wavefunction") request = GetWavefunctionRequest( program="some-program", From af16f95d707f6659c88b5c1c7a722d29706063b8 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:02:45 -0700 Subject: [PATCH 10/13] No code: add custom QPU endpoint documentation --- docs/source/advanced_usage.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/source/advanced_usage.rst b/docs/source/advanced_usage.rst index 4c2c21a01..2bb7e570e 100644 --- a/docs/source/advanced_usage.rst +++ b/docs/source/advanced_usage.rst @@ -82,6 +82,30 @@ Below is an example that demonstrates how to use pyQuil in a multithreading scen print(f"Results for program {i}:\n{result}\n") +Alternative QPU Endpoints +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Rigetti QCS supports alternative endpoints for access to a QPU architecture, useful for very particular cases. +Generally, this is useful to call "mock" or test endpoints, which simulate the results of execution for the +purposes of integration testing without the need for an active reservation or contention with other users. +See the `QCS API Docs `_ for more information on QPU Endpoints. + +To be able to call these endpoints using pyQuil, enter the ``endpoint_id`` of your desired endpoint in one +of the sites where ``quantum_processor_id`` is used: + +.. code:: python + + # Option 1 + qc = get_qc("Aspen-9", endpoint_id="my_endpoint") + + # Option 2 + qam = QPU("Aspen-9", endpoint_id="my_endpoint") + +After doing so, for all intents and purposes - compilation, optimization, etc - your program will behave the same +as when using "default" endpoint for a given quantum processor, except that it will be executed by an +alternate QCS service, and the results of execution should not be treated as correct or meaningful. + + Using Qubit Placeholders ~~~~~~~~~~~~~~~~~~~~~~~~ From 1d54f11eca8b23b5ce624003ac149429e804b3b0 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:46:28 -0700 Subject: [PATCH 11/13] No code: edit changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75bd767ac..1f11e953c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Changelog ### Improvements and Changes -- `get_qc` and `QPU` now both accept an `endpoint_id` argument which is used to engage +- Both `get_qc` and `QPU` now accept an `endpoint_id` argument which is used to engage against a specific QCS [quantum processor endpoint](https://docs.api.qcs.rigetti.com/#tag/endpoints). ### Bugfixes From 1e019fcd6ccb3ddd86dab648a05218216b984c35 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 31 Aug 2021 14:52:15 -0700 Subject: [PATCH 12/13] Fix: use named tuple for engagement cache key --- pyquil/api/_engagement_manager.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 9efe277a3..2c30f0aae 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -15,7 +15,7 @@ ############################################################################## import threading from datetime import datetime -from typing import Dict, Optional, Tuple, TYPE_CHECKING +from typing import Dict, NamedTuple, Optional, TYPE_CHECKING from dateutil.parser import parse as parsedate from dateutil.tz import tzutc @@ -30,6 +30,11 @@ import httpx +class EngagementCacheKey(NamedTuple): + quantum_processor_id: str + endpoint_id: Optional[str] + + class EngagementManager: """ Fetches (and caches) engagements for use when accessing a QPU. @@ -45,7 +50,7 @@ def __init__(self, *, client_configuration: QCSClientConfiguration) -> None: :param client_configuration: Client configuration, used for refreshing engagements. """ self._client_configuration = client_configuration - self._cached_engagements: Dict[Tuple[str, Optional[str]], EngagementWithCredentials] = {} + self._cached_engagements: Dict[EngagementCacheKey, EngagementWithCredentials] = {} self._lock = threading.Lock() def get_engagement( @@ -62,18 +67,18 @@ def get_engagement( :param request_timeout: Timeout for request, in seconds. :return: Fetched or cached engagement. """ + key = EngagementCacheKey(quantum_processor_id, endpoint_id) + with self._lock: - if not self._engagement_valid(self._cached_engagements.get((quantum_processor_id, endpoint_id))): + if not self._engagement_valid(self._cached_engagements.get(key)): with qcs_client( client_configuration=self._client_configuration, request_timeout=request_timeout ) as client: # type: httpx.Client request = CreateEngagementRequest( quantum_processor_id=quantum_processor_id, endpoint_id=endpoint_id or UNSET ) - self._cached_engagements[(quantum_processor_id, endpoint_id)] = create_engagement( - client=client, json_body=request - ).parsed - return self._cached_engagements[(quantum_processor_id, endpoint_id)] + self._cached_engagements[key] = create_engagement(client=client, json_body=request).parsed + return self._cached_engagements[key] @staticmethod def _engagement_valid(engagement: Optional[EngagementWithCredentials]) -> bool: From f19acd94bfce3d6ccdbd998ad45ed9230ad3cc37 Mon Sep 17 00:00:00 2001 From: kalzoo <22137047+kalzoo@users.noreply.github.com> Date: Tue, 31 Aug 2021 14:52:24 -0700 Subject: [PATCH 13/13] No code: improve inline docs --- pyquil/api/_engagement_manager.py | 5 +++-- pyquil/api/_qpu_client.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyquil/api/_engagement_manager.py b/pyquil/api/_engagement_manager.py index 2c30f0aae..5ff22523b 100644 --- a/pyquil/api/_engagement_manager.py +++ b/pyquil/api/_engagement_manager.py @@ -58,13 +58,14 @@ def get_engagement( ) -> EngagementWithCredentials: """ Gets an engagement for the given quantum processor endpoint. + If an engagement was already fetched previously and remains valid, it will be returned instead of creating a new engagement. - :param endpoint_id: Optional ID of the endpoint to use for engagement. If provided, it must - correspond to an endpoint serving the provided Quantum Processor. :param quantum_processor_id: Quantum processor being engaged. :param request_timeout: Timeout for request, in seconds. + :param endpoint_id: Optional ID of the endpoint to use for engagement. If provided, it must + correspond to an endpoint serving the provided Quantum Processor. :return: Fetched or cached engagement. """ key = EngagementCacheKey(quantum_processor_id, endpoint_id) diff --git a/pyquil/api/_qpu_client.py b/pyquil/api/_qpu_client.py index e1cd7e81a..b65cb2aa7 100644 --- a/pyquil/api/_qpu_client.py +++ b/pyquil/api/_qpu_client.py @@ -103,8 +103,8 @@ def __init__( self, *, quantum_processor_id: str, - endpoint_id: Optional[str] = None, engagement_manager: EngagementManager, + endpoint_id: Optional[str] = None, request_timeout: float = 10.0, ) -> None: """