diff --git a/poetry.lock b/poetry.lock index aa77746..c336801 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2376,18 +2376,20 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "qi-compute-api-client" -version = "0.39.0" +version = "0.43.0" description = "An API client for the Compute Job Manager of Quantum Inspire." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "qi_compute_api_client-0.39.0-py3-none-any.whl", hash = "sha256:5871be4c9bb4770bc87ba1a4b27cfe3638c993b3b130b9c386a6d98574f4ea6d"}, - {file = "qi_compute_api_client-0.39.0.tar.gz", hash = "sha256:5323ab5d3b2045c709781c8cb884fcd69ad5e15d1cc858cbf0e94fb12facba9a"}, + {file = "qi_compute_api_client-0.43.0-py3-none-any.whl", hash = "sha256:ea3a817c9a34ae270d8164154287029f7990cbb84e986e435b09a32b248fabf9"}, + {file = "qi_compute_api_client-0.43.0.tar.gz", hash = "sha256:1d21615ed0201d3cad6531233b6ad2c64b57dedde59da3d9e52bfb29a705a7d5"}, ] [package.dependencies] aiohttp = ">=3.10.5,<4.0.0" +pydantic = ">=2.10.4,<3.0.0" python-dateutil = ">=2.8.2,<3.0.0" +requests = ">=2.32.3,<3.0.0" urllib3 = ">=2.0.0,<3.0.0" [[package]] @@ -3366,4 +3368,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "8f599dd16fae885ddfd3578ea9436c9a1e4d9cb878c2809e3130b3d3a1e1e556" +content-hash = "c87041992003b6febc95d62a7ac33ddee134b4379f38f957332d413ea13b36ff" diff --git a/pyproject.toml b/pyproject.toml index b0fa6c9..42a22a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "qiskit-quantuminspire" -version = "0.1.0" +version = "0.3.0" description = "Qiskit provider for Quantum Inspire backends " authors = ["Quantum Inspire "] readme = "README.md" @@ -19,7 +19,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.9" qiskit = "^1.2.0" -qi-compute-api-client = "^0.39.0" +qi-compute-api-client = "^0.43.0" pydantic = "^2.10.4" requests = "^2.32.3" opensquirrel = "^0.1.0" diff --git a/qiskit_quantuminspire/api/__init__.py b/qiskit_quantuminspire/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/qiskit_quantuminspire/api/authentication.py b/qiskit_quantuminspire/api/authentication.py deleted file mode 100644 index cc29d1f..0000000 --- a/qiskit_quantuminspire/api/authentication.py +++ /dev/null @@ -1,66 +0,0 @@ -import time -from typing import Any, Tuple, cast - -import requests - -from qiskit_quantuminspire.api.settings import ApiSettings, TokenInfo, Url - - -class AuthorisationError(Exception): - """Indicates that the authorisation permanently went wrong.""" - - pass - - -class IdentityProvider: - """Class for interfacing with the IdentityProvider.""" - - def __init__(self, well_known_endpoint: str): - self._well_known_endpoint = well_known_endpoint - self._token_endpoint, self._device_endpoint = self._get_endpoints() - self._headers = {"Content-Type": "application/x-www-form-urlencoded"} - - def _get_endpoints(self) -> Tuple[str, str]: - response = requests.get(self._well_known_endpoint) - response.raise_for_status() - config = response.json() - return config["token_endpoint"], config["device_authorization_endpoint"] - - def refresh_access_token(self, client_id: str, refresh_token: str) -> dict[str, Any]: - data = { - "grant_type": "refresh_token", - "client_id": client_id, - "refresh_token": refresh_token, - } - response = requests.post(self._token_endpoint, headers=self._headers, data=data) - response.raise_for_status() - return cast(dict[str, Any], response.json()) - - -class OauthDeviceSession: - """Class for storing OAuth session information and refreshing tokens when needed.""" - - def __init__(self, host: Url, settings: ApiSettings, identity_provider: IdentityProvider): - self._api_settings = settings - _auth_settings = settings.auths[host] - self._host = host - self._client_id = _auth_settings.client_id - self._token_info = _auth_settings.tokens - self._refresh_time_reduction = 5 # the number of seconds to refresh the expiration time - self._identity_provider = identity_provider - - def refresh(self) -> TokenInfo: - if self._token_info is None: - raise AuthorisationError("You should authenticate first before you can refresh") - - if self._token_info.access_expires_at > time.time() + self._refresh_time_reduction: - return self._token_info - - try: - self._token_info = TokenInfo( - **self._identity_provider.refresh_access_token(self._client_id, self._token_info.refresh_token) - ) - self._api_settings.store_tokens(self._host, self._token_info) - return self._token_info - except requests.HTTPError as e: - raise AuthorisationError(f"An error occurred during token refresh: {e}") diff --git a/qiskit_quantuminspire/api/client.py b/qiskit_quantuminspire/api/client.py deleted file mode 100644 index a19a033..0000000 --- a/qiskit_quantuminspire/api/client.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Any, Optional - -import compute_api_client - -from qiskit_quantuminspire.api.authentication import IdentityProvider, OauthDeviceSession -from qiskit_quantuminspire.api.settings import ApiSettings - - -class Configuration(compute_api_client.Configuration): # type: ignore[misc] - """Original Configuration class in compute_api_client does not handle refreshing bearer tokens, so we need to add - some functionality.""" - - def __init__(self, host: str, oauth_session: OauthDeviceSession, **kwargs: Any): - self._oauth_session = oauth_session - super().__init__(host=host, **kwargs) - - def auth_settings(self) -> Any: - token_info = self._oauth_session.refresh() - self.access_token = token_info.access_token - return super().auth_settings() - - -_config: Optional[Configuration] = None - - -def connect() -> None: - """Set connection configuration for the Quantum Inspire API. - - Call after logging in with the CLI. Will remove old configuration. - """ - global _config - settings = ApiSettings.from_config_file() - - tokens = settings.auths[settings.default_host].tokens - - if tokens is None: - raise ValueError("No access token found for the default host. Please connect to Quantum Inspire using the CLI.") - - host = settings.default_host - _config = Configuration( - host=host, - oauth_session=OauthDeviceSession(host, settings, IdentityProvider(settings.auths[host].well_known_endpoint)), - ) - - -def config() -> Configuration: - global _config - if _config is None: - connect() - - assert _config is not None - return _config diff --git a/qiskit_quantuminspire/api/pagination.py b/qiskit_quantuminspire/api/pagination.py deleted file mode 100644 index a160d3f..0000000 --- a/qiskit_quantuminspire/api/pagination.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Any, Awaitable, Callable, Generic, List, Optional, TypeVar, Union, cast - -from pydantic import BaseModel, Field -from typing_extensions import Annotated - -PageType = TypeVar("PageType") -ItemType = TypeVar("ItemType") - - -class PageInterface(BaseModel, Generic[ItemType]): - """The page models in the generated API client don't inherit from a common base class, so we have to trick the - typing system a bit with this fake base class.""" - - items: List[ItemType] - total: Optional[Annotated[int, Field(strict=True, ge=0)]] - page: Optional[Annotated[int, Field(strict=True, ge=1)]] - size: Optional[Annotated[int, Field(strict=True, ge=1)]] - pages: Optional[Annotated[int, Field(strict=True, ge=0)]] = None - - -class PageReader(Generic[PageType, ItemType]): - """Helper class for reading fastapi-pagination style pages returned by the compute_api_client.""" - - async def get_all(self, api_call: Callable[..., Awaitable[PageType]], **kwargs: Any) -> List[ItemType]: - """Get all items from an API call that supports paging.""" - items: List[ItemType] = [] - page = 1 - - while True: - response = cast(PageInterface[ItemType], await api_call(page=page, **kwargs)) - - items.extend(response.items) - page += 1 - if response.pages is None or page > response.pages: - break - return items - - async def get_single(self, api_call: Callable[..., Awaitable[PageType]], **kwargs: Any) -> Union[ItemType, None]: - """Get a single item from an API call that supports paging.""" - response = cast(PageInterface[ItemType], await api_call(**kwargs)) - if len(response.items) > 1: - raise RuntimeError(f"Response contains more than one item -> {kwargs}.") - - return response.items[0] if response.items else None diff --git a/qiskit_quantuminspire/api/settings.py b/qiskit_quantuminspire/api/settings.py deleted file mode 100644 index dc7036e..0000000 --- a/qiskit_quantuminspire/api/settings.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Module containing the handler for the Quantum Inspire persistent configuration.""" - -from __future__ import annotations - -import time -from pathlib import Path -from typing import Dict, Optional - -from pydantic import BaseModel, BeforeValidator, Field, HttpUrl -from typing_extensions import Annotated - -Url = Annotated[str, BeforeValidator(lambda value: str(HttpUrl(value)).rstrip("/"))] -API_SETTINGS_FILE = Path.joinpath(Path.home(), ".quantuminspire", "config.json") - - -class TokenInfo(BaseModel): - """A pydantic model for storing all information regarding oauth access and refresh tokens.""" - - access_token: str - expires_in: int # [s] - refresh_token: str - refresh_expires_in: Optional[int] = None # [s] - generated_at: float = Field(default_factory=time.time) - - @property - def access_expires_at(self) -> float: - """Unix timestamp containing the time when the access token will expire.""" - return self.generated_at + self.expires_in - - -class AuthSettings(BaseModel): - """Pydantic model for storing all auth related settings for a given host.""" - - client_id: str - code_challenge_method: str - code_verifyer_length: int - well_known_endpoint: Url - tokens: Optional[TokenInfo] - team_member_id: Optional[int] - - -class ApiSettings(BaseModel): - """The settings class for the Quantum Inspire persistent configuration.""" - - auths: Dict[Url, AuthSettings] - default_host: Url - - def store_tokens(self, host: Url, tokens: TokenInfo, path: Path = API_SETTINGS_FILE) -> None: - """Stores the team_member_id, access and refresh tokens in the config.json file. - - Args: - host: The hostname of the API for which the tokens are intended. - tokens: OAuth access and refresh tokens. - path: The path to the config.json file. Defaults to API_SETTINGS_FILE. - Returns: - None - """ - self.auths[host].tokens = tokens - path.write_text(self.model_dump_json(indent=2)) - - @classmethod - def from_config_file(cls, path: Path = API_SETTINGS_FILE) -> ApiSettings: - """Load the configuration from a file.""" - if not path.is_file(): - raise FileNotFoundError("No configuration file found. Please connect to Quantum Inspire using the CLI.") - - api_settings = path.read_text() - return ApiSettings.model_validate_json(api_settings) diff --git a/qiskit_quantuminspire/qi_backend.py b/qiskit_quantuminspire/qi_backend.py index 81b8e01..31f477d 100644 --- a/qiskit_quantuminspire/qi_backend.py +++ b/qiskit_quantuminspire/qi_backend.py @@ -4,6 +4,7 @@ from typing import Any, List, Union from compute_api_client import ApiClient, BackendStatus, BackendType, BackendTypesApi +from qi2_shared.client import config from qiskit.circuit import Instruction, Measure, QuantumCircuit from qiskit.circuit.library import ( CCXGate, @@ -21,7 +22,6 @@ from qiskit.providers.options import Options from qiskit.transpiler import CouplingMap, Target -from qiskit_quantuminspire.api.client import config from qiskit_quantuminspire.qi_jobs import QIJob from qiskit_quantuminspire.utils import is_coupling_map_complete, run_async diff --git a/qiskit_quantuminspire/qi_jobs.py b/qiskit_quantuminspire/qi_jobs.py index 0b6dcbb..696ba36 100644 --- a/qiskit_quantuminspire/qi_jobs.py +++ b/qiskit_quantuminspire/qi_jobs.py @@ -35,6 +35,9 @@ ResultsApi, ShareType, ) +from qi2_shared.client import config +from qi2_shared.pagination import PageReader +from qi2_shared.settings import ApiSettings from qiskit import qpy from qiskit.circuit import QuantumCircuit from qiskit.providers import JobV1 @@ -45,9 +48,6 @@ from qiskit.result.result import Result from qiskit_quantuminspire import cqasm -from qiskit_quantuminspire.api.client import config -from qiskit_quantuminspire.api.pagination import PageReader -from qiskit_quantuminspire.api.settings import ApiSettings from qiskit_quantuminspire.base_provider import BaseProvider from qiskit_quantuminspire.utils import run_async diff --git a/qiskit_quantuminspire/qi_provider.py b/qiskit_quantuminspire/qi_provider.py index 0a38860..890a7a3 100644 --- a/qiskit_quantuminspire/qi_provider.py +++ b/qiskit_quantuminspire/qi_provider.py @@ -1,9 +1,9 @@ from typing import Any, List, Optional, Sequence from compute_api_client import ApiClient, BackendType, BackendTypesApi, PageBackendType +from qi2_shared.client import config +from qi2_shared.pagination import PageReader -from qiskit_quantuminspire.api.client import config -from qiskit_quantuminspire.api.pagination import PageReader from qiskit_quantuminspire.base_provider import BaseProvider from qiskit_quantuminspire.qi_backend import QIBackend from qiskit_quantuminspire.utils import run_async diff --git a/tests/api/conftest.py b/tests/api/conftest.py deleted file mode 100644 index 83768ea..0000000 --- a/tests/api/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - -from qiskit_quantuminspire.api.settings import AuthSettings, TokenInfo - - -@pytest.fixture -def token_info() -> TokenInfo: - return TokenInfo( - access_token="access_token", - expires_in=100, - refresh_token="refresh_token", - refresh_expires_in=1000, - generated_at=1, - ) - - -@pytest.fixture -def auth_settings(token_info: TokenInfo) -> AuthSettings: - return AuthSettings( - client_id="client_id", - code_challenge_method="code_challenge_method", - code_verifyer_length=1, - well_known_endpoint="https://host.com/well-known-endpoint", - tokens=token_info, - team_member_id=1, - ) diff --git a/tests/api/test_authentication.py b/tests/api/test_authentication.py deleted file mode 100644 index d82ced5..0000000 --- a/tests/api/test_authentication.py +++ /dev/null @@ -1,108 +0,0 @@ -import time -from typing import Any -from unittest.mock import MagicMock - -import pytest -import responses -from responses import matchers - -from qiskit_quantuminspire.api.authentication import AuthorisationError, IdentityProvider, OauthDeviceSession -from qiskit_quantuminspire.api.settings import ApiSettings, AuthSettings, TokenInfo - - -@pytest.fixture -def identity_provider_mock() -> MagicMock: - return MagicMock(spec=IdentityProvider) - - -@pytest.fixture -def api_settings_mock(auth_settings: AuthSettings) -> MagicMock: - api_settings = MagicMock(spec=ApiSettings) - api_settings.default_host = "https://host.com" - api_settings.auths = {api_settings.default_host: auth_settings} - return api_settings - - -def test_oauth_device_session_refresh_no_token(api_settings_mock: MagicMock, identity_provider_mock: MagicMock) -> None: - # Arrange - api_settings_mock.auths[api_settings_mock.default_host].tokens = None - session = OauthDeviceSession("https://host.com", api_settings_mock, identity_provider_mock) - - # Act & Assert - with pytest.raises(AuthorisationError): - session.refresh() - - -def test_oauth_device_session_refresh_token_not_expired( - api_settings_mock: MagicMock, identity_provider_mock: MagicMock -) -> None: - # Arrange - auth_settings = api_settings_mock.auths[api_settings_mock.default_host] - auth_settings.tokens.generated_at = time.time() - session = OauthDeviceSession("https://host.com", api_settings_mock, identity_provider_mock) - - # Act - token_info = session.refresh() - - # Assert - assert token_info == auth_settings.tokens - - identity_provider_mock.refresh_access_token.assert_not_called() - - -def test_oauth_device_session_refresh_token_expired( - api_settings_mock: MagicMock, identity_provider_mock: MagicMock -) -> None: - # Arrange - session = OauthDeviceSession("https://host.com", api_settings_mock, identity_provider_mock) - new_token_info: dict[str, Any] = { - "access_token": "new_access_token", - "expires_in": 100, - "refresh_token": "new_refresh_token", - "refresh_expires_in": 1000, - "generated_at": time.time(), - } - - identity_provider_mock.refresh_access_token.return_value = new_token_info - - # Act - token_info = session.refresh() - - # Assert - assert token_info == TokenInfo(**new_token_info) - - identity_provider_mock.refresh_access_token.assert_called_once_with("client_id", "refresh_token") - api_settings_mock.store_tokens.assert_called_once_with("https://host.com", token_info) - - -@responses.activate -def test_identity_provider_refresh_access_token() -> None: - # Arrange - token_info = {"token": "something", "some": "other_data"} - client_id = "some_client" - old_refresh_token = "old_token" - - responses.get( - "https://host.com/well-known-endpoint", - json={ - "token_endpoint": "https://host.com/token-endpoint", - "device_authorization_endpoint": "https://host.com/device-endpoint", - }, - ) - responses.post( - "https://host.com/token-endpoint", - json=token_info, - match=[ - matchers.urlencoded_params_matcher( - {"grant_type": "refresh_token", "client_id": client_id, "refresh_token": old_refresh_token} - ) - ], - ) - - # Act - provider = IdentityProvider("https://host.com/well-known-endpoint") - - token = provider.refresh_access_token(client_id, old_refresh_token) - - # Assert - assert token == token_info diff --git a/tests/api/test_client.py b/tests/api/test_client.py deleted file mode 100644 index dbc10f2..0000000 --- a/tests/api/test_client.py +++ /dev/null @@ -1,54 +0,0 @@ -from unittest.mock import MagicMock - -import pytest -from pytest_mock import MockerFixture - -from qiskit_quantuminspire.api.client import Configuration, config, connect -from qiskit_quantuminspire.api.settings import ApiSettings, AuthSettings, TokenInfo - - -@pytest.fixture -def api_settings_mock(auth_settings: AuthSettings, mocker: MockerFixture) -> MagicMock: - api_settings = MagicMock(spec=ApiSettings) - api_settings.default_host = "https://host.com" - api_settings.auths = {api_settings.default_host: auth_settings} - api_settings.from_config_file.return_value = api_settings - - mocker.patch("qiskit_quantuminspire.api.client.ApiSettings", return_value=api_settings) - - return api_settings - - -def test_connect_no_tokens(api_settings_mock: MagicMock) -> None: - # Arrange - api_settings_mock.auths[api_settings_mock.default_host].tokens = None - - # Act & Assert - with pytest.raises(ValueError): - connect() - - -def test_config(api_settings_mock: MagicMock, mocker: MockerFixture) -> None: - # Arrange - session = MagicMock() - mocker.patch("qiskit_quantuminspire.api.client.OauthDeviceSession", return_value=session) - mocker.patch("qiskit_quantuminspire.api.client.IdentityProvider") - - # Act - conf = config() - - # Assert - assert conf._oauth_session == session - - -def test_configuration_auth_settings(token_info: TokenInfo) -> None: - # Arrange - session = MagicMock() - session.refresh.return_value = token_info - config = Configuration(host="host", oauth_session=session) - - # Act - config.auth_settings() - - # Assert - assert config.access_token == token_info.access_token diff --git a/tests/api/test_pagination.py b/tests/api/test_pagination.py deleted file mode 100644 index bd0f3f1..0000000 --- a/tests/api/test_pagination.py +++ /dev/null @@ -1,110 +0,0 @@ -from unittest.mock import AsyncMock - -import pytest -from compute_api_client import BackendType, BatchJob, PageBackendType, PageBatchJob -from pytest_mock import MockerFixture - -from qiskit_quantuminspire.api.pagination import PageReader -from tests.helpers import create_backend_type - - -@pytest.mark.asyncio -async def test_pagination_get_all() -> None: - # Arrange - def returned_pages(page: int) -> PageBackendType: - pages = [ - PageBackendType( - items=[ - create_backend_type(name="qi_backend_1"), - create_backend_type(name="spin"), - ], - total=5, - page=1, - size=2, - pages=2, - ), - PageBackendType( - items=[ - create_backend_type(name="qi_backend2"), - create_backend_type(name="spin6"), - create_backend_type(name="spin7"), - ], - total=5, - page=2, - size=3, - pages=2, - ), - ] - return pages[page - 1] - - api_call = AsyncMock(side_effect=returned_pages) - - page_reader = PageReader[PageBackendType, BackendType]() - - # Act - backends = await page_reader.get_all(api_call) - - # Assert - actual_backend_names = [backend.name for backend in backends] - expected_backend_names = ["qi_backend_1", "spin", "qi_backend2", "spin6", "spin7"] - - assert actual_backend_names == expected_backend_names - - -@pytest.mark.asyncio -async def test_pagination_get_single_error(mocker: MockerFixture) -> None: - batchjob_patch = mocker.patch( - "compute_api_client.BatchJob", - autospec=True, - ) - - # Arrange - def returned_jobs() -> PageBatchJob: - pages = [ - PageBatchJob( - items=[batchjob_patch, batchjob_patch, batchjob_patch], - total=5, - page=1, - size=3, - pages=2, - ) - ] - return pages[0] - - api_call = AsyncMock(side_effect=returned_jobs) - - page_reader = PageReader[PageBatchJob, BatchJob]() - - # Act - with pytest.raises(RuntimeError): - await page_reader.get_single(api_call) - - -@pytest.mark.asyncio -async def test_pagination_get_single(mocker: MockerFixture) -> None: - batchjob_patch = mocker.patch( - "compute_api_client.BatchJob", - autospec=True, - ) - - # Arrange - def returned_jobs() -> PageBatchJob: - pages = [ - PageBatchJob( - items=[batchjob_patch], - total=1, - page=1, - size=1, - pages=1, - ) - ] - return pages[0] - - api_call = AsyncMock(side_effect=returned_jobs) - - page_reader = PageReader[PageBatchJob, BatchJob]() - - # Act - batchjob = await page_reader.get_single(api_call) - - assert batchjob == batchjob_patch diff --git a/tests/api/test_settings.py b/tests/api/test_settings.py deleted file mode 100644 index 2191ddf..0000000 --- a/tests/api/test_settings.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -import pytest - -from qiskit_quantuminspire.api.settings import ApiSettings, AuthSettings - - -def test_store_tokens(auth_settings: AuthSettings, tmpdir: str) -> None: - # Arrange - host = "https://host.com" - api_settings = ApiSettings(auths={host: auth_settings}, default_host=host) - stored_tokens_path = Path(tmpdir.join("tokens.json")) - - # Act - assert auth_settings.tokens is not None - api_settings.store_tokens(host=host, tokens=auth_settings.tokens, path=stored_tokens_path) - - # Assert - assert api_settings == api_settings.model_validate_json(stored_tokens_path.read_text()) - - -def test_api_settings_read_file(auth_settings: AuthSettings, tmpdir: str) -> None: - # Arrange - settings_file = Path(tmpdir.join("config.json")) - host = "https://host.com" - settings_stored = ApiSettings(auths={host: auth_settings}, default_host=host) - settings_file.write_text(settings_stored.model_dump_json()) - - # Act - settings_read = ApiSettings.from_config_file(path=settings_file) - - # Assert - assert settings_stored == settings_read - - -def test_api_settings_no_configuration_file(tmpdir: str) -> None: - # Arrange - settings_file = Path(tmpdir.join("non-existent.json")) - - # Act & Assert - with pytest.raises(FileNotFoundError): - ApiSettings.from_config_file(path=settings_file) diff --git a/tests/conftest.py b/tests/conftest.py index eed92da..431f2b4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,7 @@ import pytest from compute_api_client import BatchJobStatus from pytest_mock import MockerFixture - -from qiskit_quantuminspire.api.pagination import PageReader +from qi2_shared.pagination import PageReader @pytest.fixture diff --git a/tests/test_qi_provider.py b/tests/test_qi_provider.py index 9d1f69c..34b8ea3 100644 --- a/tests/test_qi_provider.py +++ b/tests/test_qi_provider.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock import pytest +from pytest_mock import MockerFixture from qiskit_quantuminspire.qi_backend import QIBackend from qiskit_quantuminspire.qi_provider import QIProvider @@ -9,7 +10,9 @@ @pytest.fixture -def backend_repository(mock_job_api: Any, page_reader_mock: AsyncMock) -> None: +def backend_repository(mocker: MockerFixture, mock_job_api: Any, page_reader_mock: AsyncMock) -> None: + mocker.patch("qiskit_quantuminspire.qi_provider.config") + mocker.patch("qiskit_quantuminspire.qi_provider.ApiClient") page_reader_mock.get_all.return_value = [ create_backend_type(name="qi_backend_1", id=10), create_backend_type(name="spin", id=20),