diff --git a/docs/apidoc/api_client.rst b/docs/apidoc/api_client.rst new file mode 100644 index 0000000..52cf334 --- /dev/null +++ b/docs/apidoc/api_client.rst @@ -0,0 +1,25 @@ +.. _api_client: + +========== +API client +========== + +.. autoclass:: qiskit_aqt_provider.api_client.portal_client.PortalClient + :members: + :show-inheritance: + :inherited-members: + +.. autoclass:: qiskit_aqt_provider.api_client.models.Workspaces + :members: + :show-inheritance: + :exclude-members: __init__, __new__, model_fields, model_computed_fields, model_config + +.. autoclass:: qiskit_aqt_provider.api_client.models.Workspace + :members: + :show-inheritance: + :exclude-members: __init__, __new__, model_fields, model_computed_fields, model_config + +.. autoclass:: qiskit_aqt_provider.api_client.models.Resource + :members: + :show-inheritance: + :exclude-members: __init__, __new__, model_fields, model_computed_fields, model_config diff --git a/docs/conf.py b/docs/conf.py index dc53c90..7967748 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,6 +57,10 @@ ("py:class", "Target"), ("py:exc", "QiskitBackendNotFoundError"), ("py:class", "qiskit_aqt_provider.aqt_resource._OptionsType"), + # No inventory available for httpx + # https://github.com/encode/httpx/issues/3145 + ("py:exc", "httpx.NetworkError"), + ("py:exc", "httpx.HTTPStatusError"), ] nitpick_ignore_regex = [ ("py:class", r"qiskit_aqt_provider\.api_models_generated.*"), @@ -83,4 +87,5 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "qiskit": ("https://docs.quantum.ibm.com/api/qiskit/", None), + "pydantic": ("https://docs.pydantic.dev/latest/", None), } diff --git a/docs/index.rst b/docs/index.rst index 08aaba8..86fb9c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -72,6 +72,7 @@ For more details see the :ref:`user guide `, a selection of `example Options Qiskit primitives Transpiler plugin + API client .. toctree:: :hidden: diff --git a/qiskit_aqt_provider/api_client/__init__.py b/qiskit_aqt_provider/api_client/__init__.py index c7f61e9..c13ccf7 100644 --- a/qiskit_aqt_provider/api_client/__init__.py +++ b/qiskit_aqt_provider/api_client/__init__.py @@ -7,3 +7,17 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. + +from .models import Resource, ResourceType, Workspace, Workspaces +from .portal_client import DEFAULT_PORTAL_URL, PortalClient +from .versions import __version__ + +__all__ = [ + "DEFAULT_PORTAL_URL", + "PortalClient", + "Resource", + "ResourceType", + "Workspace", + "Workspaces", + "__version__", +] diff --git a/qiskit_aqt_provider/api_client/models.py b/qiskit_aqt_provider/api_client/models.py index 1267da9..952d168 100644 --- a/qiskit_aqt_provider/api_client/models.py +++ b/qiskit_aqt_provider/api_client/models.py @@ -15,6 +15,8 @@ import importlib.metadata import platform import re +import typing +from collections.abc import Collection, Iterator from re import Pattern from typing import Any, Final, Literal, Optional, Union from uuid import UUID @@ -22,7 +24,7 @@ import httpx import pydantic as pdt from qiskit.providers.exceptions import JobError -from typing_extensions import Self, TypeAlias +from typing_extensions import Self, TypeAlias, override from . import models_generated as api_models from .models_generated import ( @@ -32,7 +34,6 @@ QuantumCircuits, SubmitJobRequest, ) -from .models_generated import Type as ResourceType __all__ = [ "Circuit", @@ -43,7 +44,6 @@ "QuantumCircuit", "QuantumCircuits", "Response", - "ResourceType", ] @@ -78,11 +78,153 @@ def http_client( return httpx.Client(headers=headers, base_url=base_url, timeout=10.0, follow_redirects=True) -class Workspaces(pdt.RootModel[list[api_models.Workspace]]): - """List of available workspaces and devices.""" +ResourceType: TypeAlias = Literal["device", "simulator", "offline_simulator"] + + +class Resource(pdt.BaseModel): + """Description of a resource. + + This is the element type in :py:attr:`Workspace.resources`. + """ + + model_config = pdt.ConfigDict(frozen=True) + + workspace_id: str + """Identifier of the workspace this resource belongs to.""" + + resource_id: str + """Resource identifier.""" + + resource_name: str + """Resource name.""" + + resource_type: ResourceType + """Type of resource.""" + + +class Workspace(pdt.BaseModel): + """Description of a workspace and the resources it contains. + + This is the element type in the :py:class:`Workspaces` container. + """ + + model_config = pdt.ConfigDict(frozen=True) + + workspace_id: str + """Workspace identifier.""" + + resources: list[Resource] + """Resources in the workspace.""" + + +class Workspaces( + pdt.RootModel, # type: ignore[type-arg] + Collection[Workspace], +): + """List of available workspaces and devices. + + .. + >>> workspaces = Workspaces( + ... root=[ + ... api_models.Workspace( + ... id="workspace0", + ... resources=[ + ... api_models.Resource( + ... id="resource0", + ... name="resource0", + ... type=api_models.Type.device, + ... ), + ... ], + ... ), + ... api_models.Workspace( + ... id="workspace1", + ... resources=[ + ... api_models.Resource( + ... id="resource0", + ... name="resource0", + ... type=api_models.Type.device, + ... ), + ... api_models.Resource( + ... id="resource1", + ... name="resource1", + ... type=api_models.Type.simulator, + ... ), + ... ], + ... ), + ... ] + ... ) + + Examples: + Assume a :py:class:`Workspaces` instance retrieved from the API with + the following contents: + + .. code-block:: + + | Workspace ID | Resource ID | Resource Type | + |--------------+-------------+---------------| + | workspace0 | resource0 | device | + | workspace1 | resource0 | device | + | workspace1 | resource1 | simulator | + + Gather basic information: + + >>> # workspaces = PortalClient(...).workspaces() + >>> len(workspaces) + 2 + >>> [ws.workspace_id for ws in workspaces] + ['workspace0', 'workspace1'] + + Inclusion tests rely only on the identifier: + + >>> Workspace(workspace_id="workspace0", resources=[]) in workspaces + True + + The :py:meth:`Workspaces.filter` method allows for complex filtering. For example + by workspace identifier ending in ``0``: + + >>> [ws.workspace_id for ws in workspaces.filter(workspace_pattern=re.compile(".+0$"))] + ['workspace0'] + + or only the non-simulated devices: + + >>> workspaces_devices = workspaces.filter(backend_type="device") + >>> [(ws.workspace_id, resource.resource_id) + ... for ws in workspaces_devices for resource in ws.resources] + [('workspace0', 'resource0'), ('workspace1', 'resource0')] + """ root: list[api_models.Workspace] + @override + def __len__(self) -> int: + """Number of available workspaces.""" + return len(self.root) + + @override + def __iter__(self) -> Iterator[Workspace]: # type: ignore[override] + """Iterator over the workspaces.""" + for ws in self.root: + yield Workspace( + workspace_id=ws.id, + resources=[ + Resource( + workspace_id=ws.id, + resource_id=res.id, + resource_name=res.name, + resource_type=typing.cast(ResourceType, res.type.value), + ) + for res in ws.resources + ], + ) + + @override + def __contains__(self, obj: object) -> bool: + """Whether a given workspace is in this workspaces collection.""" + if not isinstance(obj, Workspace): # pragma: no cover + return False + + return any(ws.id == obj.workspace_id for ws in self.root) + def filter( self, *, @@ -100,7 +242,7 @@ def filter( backend_type: backend type to select. Returns: - Workspaces model that only contains matching resources. + :py:class:`Workspaces` instance that only contains matching resources. """ filtered_workspaces = [] for workspace in self.root: @@ -110,7 +252,7 @@ def filter( filtered_resources = [] for resource in workspace.resources: - if backend_type is not None and resource.type is not backend_type: + if backend_type is not None and resource.type.value != backend_type: continue if name_pattern is not None and not re.match(name_pattern, resource.id): @@ -125,27 +267,6 @@ def filter( return self.__class__(root=filtered_workspaces) -GeneralResourceType: TypeAlias = Literal["device", "simulator", "offline_simulator"] - - -class ResourceId(pdt.BaseModel): - """Resource identification and metadata.""" - - model_config = pdt.ConfigDict(frozen=True) - - workspace_id: str - """Workspace containing the resource.""" - - resource_id: str - """Unique identifier of the resource in the containing workspace.""" - - resource_name: str - """Pretty display name for the resource.""" - - resource_type: GeneralResourceType - """Resource type, also includes offline simulators.""" - - class Operation: """Factories for API payloads of circuit operations.""" diff --git a/qiskit_aqt_provider/api_client/portal_client.py b/qiskit_aqt_provider/api_client/portal_client.py new file mode 100644 index 0000000..0feabfd --- /dev/null +++ b/qiskit_aqt_provider/api_client/portal_client.py @@ -0,0 +1,67 @@ +# (C) Copyright Alpine Quantum Technologies GmbH 2024 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import os +from typing import Final, Optional + +import httpx + +from . import models +from .versions import make_user_agent + +DEFAULT_PORTAL_URL: Final = httpx.URL("https://arnica.aqt.eu") +"""Default URL for the remote portal.""" + + +class PortalClient: + """Client for the AQT portal API.""" + + USER_AGENT_NAME: Final = "aqt-portal-client" + + def __init__( + self, *, token: str, user_agent_extra: Optional[str] = None, timeout: Optional[float] = 10.0 + ) -> None: + """Initialize a new client for the AQT remote computing portal API. + + The portal base URL can be overridden using the ``AQT_PORTAL_URL`` + environment variable. + + Args: + token: authentication token. + user_agent_extra: data appended to the default user-agent string. + timeout: HTTP timeout, in seconds. + """ + self.portal_url = httpx.URL(os.environ.get("AQT_PORTAL_URL", DEFAULT_PORTAL_URL)) + + user_agent = make_user_agent(self.USER_AGENT_NAME, extra=user_agent_extra) + headers = {"User-Agent": user_agent} + + if token: + headers["Authorization"] = f"Bearer {token}" + + self._http_client = httpx.Client( + base_url=self.portal_url.join("/api/v1"), + headers=headers, + timeout=timeout, + follow_redirects=True, + ) + + def workspaces(self) -> models.Workspaces: + """List the workspaces visible to the used token. + + Raises: + httpx.NetworkError: connection to the remote portal failed. + httpx.HTTPStatusError: something went wrong with the request to the remote portal. + """ + with self._http_client as client: + response = client.get("/workspaces") + + response.raise_for_status() + return models.Workspaces.model_validate(response.json()) diff --git a/qiskit_aqt_provider/api_client/versions.py b/qiskit_aqt_provider/api_client/versions.py new file mode 100644 index 0000000..ad4671c --- /dev/null +++ b/qiskit_aqt_provider/api_client/versions.py @@ -0,0 +1,37 @@ +# (C) Copyright Alpine Quantum Technologies GmbH 2024 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +import importlib.metadata +import platform +from typing import Final, Optional + +PACKAGE_VERSION: Final = importlib.metadata.version("qiskit-aqt-provider") +__version__: Final = PACKAGE_VERSION + + +def make_user_agent(name: str, *, extra: Optional[str] = None) -> str: + """User-agent strings factory. + + Args: + name: main name of the component to build a user-agent string for. + extra: arbitrary extra data, appended to the default string. + """ + user_agent = " ".join( + [ + f"{name}/{PACKAGE_VERSION}", + f"({platform.system()};", + f"{platform.python_implementation()}/{platform.python_version()})", + ] + ) + + if extra: + user_agent += f" {extra}" + + return user_agent diff --git a/qiskit_aqt_provider/aqt_provider.py b/qiskit_aqt_provider/aqt_provider.py index f2b127e..874d37b 100644 --- a/qiskit_aqt_provider/aqt_provider.py +++ b/qiskit_aqt_provider/aqt_provider.py @@ -16,14 +16,13 @@ import re import warnings from collections import defaultdict -from collections.abc import Sequence +from collections.abc import Iterable, Sequence from dataclasses import dataclass from operator import attrgetter from pathlib import Path from re import Pattern from typing import ( Final, - Literal, Optional, Union, overload, @@ -35,7 +34,7 @@ from tabulate import tabulate from typing_extensions import TypeAlias, override -from qiskit_aqt_provider.api_client import models as api_models +from qiskit_aqt_provider.api_client import PortalClient, Resource, ResourceType, Workspace from qiskit_aqt_provider.aqt_resource import ( AQTDirectAccessResource, AQTResource, @@ -146,9 +145,6 @@ def table(self) -> list[list[str]]: class AQTProvider: """Provider for backends from Alpine Quantum Technologies (AQT).""" - # Set AQT_PORTAL_URL environment variable to override - DEFAULT_PORTAL_URL: Final = "https://arnica.aqt.eu" - def __init__( self, access_token: Optional[str] = None, @@ -183,9 +179,6 @@ def __init__( if load_dotenv or dotenv_path is not None: dotenv.load_dotenv(dotenv_path) - portal_base_url = os.environ.get("AQT_PORTAL_URL", AQTProvider.DEFAULT_PORTAL_URL) - self.portal_url = f"{portal_base_url}/api/v1" - if access_token is None: self.access_token = os.environ.get("AQT_TOKEN", "") else: @@ -200,17 +193,18 @@ def __init__( self.name = "aqt_provider" @property - def _http_client(self) -> httpx.Client: - """HTTP client for communicating with the AQT cloud service.""" - return api_models.http_client( - base_url=self.portal_url, token=self.access_token, user_agent_extra=USER_AGENT_EXTRA + def _portal_client(self) -> PortalClient: + """API client.""" + return PortalClient( + token=self.access_token, + user_agent_extra=USER_AGENT_EXTRA, ) def backends( self, name: Optional[Union[str, Pattern[str]]] = None, *, - backend_type: Optional[Literal["device", "simulator", "offline_simulator"]] = None, + backend_type: Optional[ResourceType] = None, workspace: Optional[Union[str, Pattern[str]]] = None, ) -> BackendsTable: """Search for cloud backends matching given criteria. @@ -236,17 +230,14 @@ def backends( if isinstance(workspace, str): workspace = re.compile(f"^{workspace}$") - remote_workspaces = api_models.Workspaces(root=[]) + remote_workspaces: Iterable[Workspace] = [] + # Only query if remote resources are requested. if backend_type != "offline_simulator": with contextlib.suppress(httpx.HTTPError, httpx.NetworkError): - with self._http_client as client: - resp = client.get("/workspaces") - resp.raise_for_status() - - remote_workspaces = api_models.Workspaces.model_validate(resp.json()).filter( + remote_workspaces = self._portal_client.workspaces().filter( name_pattern=name, - backend_type=api_models.ResourceType(backend_type) if backend_type else None, + backend_type=backend_type if backend_type else None, workspace_pattern=workspace, ) @@ -262,7 +253,7 @@ def backends( backends.append( OfflineSimulatorResource( self, - resource_id=api_models.ResourceId( + resource_id=Resource( workspace_id="default", resource_id=simulator.id, resource_name=simulator.name, @@ -278,14 +269,9 @@ def backends( + [ AQTResource( self, - resource_id=api_models.ResourceId( - workspace_id=_workspace.id, - resource_id=resource.id, - resource_name=resource.name, - resource_type=resource.type.value, - ), + resource_id=resource, ) - for _workspace in remote_workspaces.root + for _workspace in remote_workspaces for resource in _workspace.resources ] ) @@ -294,7 +280,7 @@ def get_backend( self, name: Optional[Union[str, Pattern[str]]] = None, *, - backend_type: Optional[Literal["device", "simulator", "offline_simulator"]] = None, + backend_type: Optional[ResourceType] = None, workspace: Optional[Union[str, Pattern[str]]] = None, ) -> AQTResource: """Return a handle for a cloud quantum computing resource matching the specified filtering. diff --git a/qiskit_aqt_provider/aqt_resource.py b/qiskit_aqt_provider/aqt_resource.py index cd2bfad..023332c 100644 --- a/qiskit_aqt_provider/aqt_resource.py +++ b/qiskit_aqt_provider/aqt_resource.py @@ -35,6 +35,7 @@ from qiskit_aer import AerJob, AerSimulator, noise from typing_extensions import override +from qiskit_aqt_provider import api_client from qiskit_aqt_provider.api_client import models as api_models from qiskit_aqt_provider.api_client import models_direct as api_models_direct from qiskit_aqt_provider.aqt_job import AQTDirectAccessJob, AQTJob @@ -107,7 +108,7 @@ def __init__( { "backend_name": name, "backend_version": 2, - "url": provider.portal_url, + "url": str(provider._portal_client.portal_url), "simulator": True, "local": False, "coupling_map": None, @@ -209,7 +210,7 @@ class AQTResource(_ResourceBase[AQTOptions]): def __init__( self, provider: "AQTProvider", - resource_id: api_models.ResourceId, + resource_id: api_client.Resource, ) -> None: """Initialize the backend. @@ -223,7 +224,7 @@ def __init__( options_type=AQTOptions, ) - self._http_client: httpx.Client = provider._http_client + self._http_client: httpx.Client = provider._portal_client._http_client self.resource_id = resource_id def run(self, circuits: Union[QuantumCircuit, list[QuantumCircuit]], **options: Any) -> AQTJob: @@ -434,7 +435,7 @@ class OfflineSimulatorResource(AQTResource): def __init__( self, provider: "AQTProvider", - resource_id: api_models.ResourceId, + resource_id: api_client.Resource, with_noise_model: bool, ) -> None: """Initialize an offline simulator resource. diff --git a/qiskit_aqt_provider/persistence.py b/qiskit_aqt_provider/persistence.py index 8c890a0..7312223 100644 --- a/qiskit_aqt_provider/persistence.py +++ b/qiskit_aqt_provider/persistence.py @@ -24,7 +24,7 @@ from qiskit.circuit import QuantumCircuit from typing_extensions import Self -from qiskit_aqt_provider.api_client.models import ResourceId +from qiskit_aqt_provider.api_client import Resource from qiskit_aqt_provider.aqt_options import AQTOptions from qiskit_aqt_provider.utils import map_exceptions from qiskit_aqt_provider.versions import QISKIT_AQT_PROVIDER_VERSION @@ -91,7 +91,7 @@ class Job(pdt.BaseModel): model_config = ConfigDict(frozen=True, json_encoders={Circuits: Circuits.json_encoder}) - resource: ResourceId + resource: Resource circuits: Circuits options: AQTOptions diff --git a/qiskit_aqt_provider/test/fixtures.py b/qiskit_aqt_provider/test/fixtures.py index 5d1d4ee..1992b1c 100644 --- a/qiskit_aqt_provider/test/fixtures.py +++ b/qiskit_aqt_provider/test/fixtures.py @@ -27,6 +27,7 @@ from qiskit_aer import AerSimulator from typing_extensions import override +from qiskit_aqt_provider import api_client from qiskit_aqt_provider.api_client import models as api_models from qiskit_aqt_provider.api_client import models_direct as api_models_direct from qiskit_aqt_provider.aqt_job import AQTJob @@ -47,7 +48,7 @@ def __init__(self, *, noisy: bool) -> None: """Initialize the mocked simulator backend.""" super().__init__( AQTProvider(""), - resource_id=api_models.ResourceId( + resource_id=api_client.Resource( workspace_id="default", resource_id="mock_simulator", resource_name="mock_simulator", diff --git a/qiskit_aqt_provider/test/resources.py b/qiskit_aqt_provider/test/resources.py index 47d72a9..8a4363a 100644 --- a/qiskit_aqt_provider/test/resources.py +++ b/qiskit_aqt_provider/test/resources.py @@ -22,6 +22,7 @@ from qiskit import QuantumCircuit from typing_extensions import assert_never, override +from qiskit_aqt_provider import api_client from qiskit_aqt_provider.api_client import models as api_models from qiskit_aqt_provider.aqt_job import AQTJob from qiskit_aqt_provider.aqt_provider import AQTProvider @@ -151,7 +152,7 @@ def __init__( """ super().__init__( AQTProvider(""), - resource_id=api_models.ResourceId( + resource_id=api_client.Resource( workspace_id="test-workspace", resource_id="test", resource_name="test-resource", @@ -222,7 +223,7 @@ def __init__(self, token: str) -> None: """Initialize the dummy backend.""" super().__init__( AQTProvider(token), - resource_id=api_models.ResourceId( + resource_id=api_client.Resource( workspace_id="dummy", resource_id="dummy", resource_name="dummy", diff --git a/test/api_client/test_models.py b/test/api_client/test_models.py index 672688a..3303637 100644 --- a/test/api_client/test_models.py +++ b/test/api_client/test_models.py @@ -10,10 +10,35 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +import pytest + from qiskit_aqt_provider.api_client import models as api_models from qiskit_aqt_provider.api_client import models_generated as api_models_generated +def test_workspaces_container_empty() -> None: + """Test the empty edge case of the Workspaces container.""" + empty = api_models.Workspaces(root=[]) + assert len(empty) == 0 + + with pytest.raises(StopIteration): + next(iter(empty)) + + assert api_models.Workspace(workspace_id="w1", resources=[]) not in empty + + +def test_workspaces_contains() -> None: + """Test the Workspaces.__contains__ implementation.""" + workspaces = api_models.Workspaces( + root=[ + api_models_generated.Workspace(id="w1", resources=[]), + ] + ) + + assert api_models.Workspace(workspace_id="w1", resources=[]) in workspaces + assert api_models.Workspace(workspace_id="w2", resources=[]) not in workspaces + + def test_workspaces_filter_by_workspace() -> None: """Test filtering the Workspaces model content by workspace ID.""" workspaces = api_models.Workspaces( @@ -24,10 +49,10 @@ def test_workspaces_filter_by_workspace() -> None: ) filtered = workspaces.filter(workspace_pattern="^w") - assert {workspace.id for workspace in filtered.root} == {"w1", "w2"} + assert {workspace.workspace_id for workspace in filtered} == {"w1", "w2"} filtered = workspaces.filter(workspace_pattern="w1") - assert {workspace.id for workspace in filtered.root} == {"w1"} + assert {workspace.workspace_id for workspace in filtered} == {"w1"} def test_workspaces_filter_by_name() -> None: @@ -38,10 +63,10 @@ def test_workspaces_filter_by_name() -> None: id="w1", resources=[ api_models_generated.Resource( - id="r10", name="r10", type=api_models.ResourceType.device + id="r10", name="r10", type=api_models_generated.Type.device ), api_models_generated.Resource( - id="r20", name="r20", type=api_models.ResourceType.device + id="r20", name="r20", type=api_models_generated.Type.device ), ], ), @@ -49,7 +74,7 @@ def test_workspaces_filter_by_name() -> None: id="w2", resources=[ api_models_generated.Resource( - id="r11", name="r11", type=api_models.ResourceType.simulator + id="r11", name="r11", type=api_models_generated.Type.simulator ) ], ), @@ -64,7 +89,7 @@ def test_workspaces_filter_by_name() -> None: id="w1", resources=[ api_models_generated.Resource( - id="r10", name="r10", type=api_models.ResourceType.device + id="r10", name="r10", type=api_models_generated.Type.device ), ], ), @@ -72,7 +97,7 @@ def test_workspaces_filter_by_name() -> None: id="w2", resources=[ api_models_generated.Resource( - id="r11", name="r11", type=api_models.ResourceType.simulator + id="r11", name="r11", type=api_models_generated.Type.simulator ) ], ), @@ -88,23 +113,23 @@ def test_workspaces_filter_by_backend_type() -> None: id="w1", resources=[ api_models_generated.Resource( - id="r1", name="r1", type=api_models.ResourceType.device + id="r1", name="r1", type=api_models_generated.Type.device ), api_models_generated.Resource( - id="r2", name="r2", type=api_models.ResourceType.simulator + id="r2", name="r2", type=api_models_generated.Type.simulator ), ], ) ] ) - filtered = workspaces.filter(backend_type=api_models.ResourceType.simulator) - assert len(filtered.root) == 1 - assert filtered.root[0] == api_models_generated.Workspace( - id="w1", + filtered = workspaces.filter(backend_type="simulator") + assert len(filtered) == 1 + assert next(iter(filtered)) == api_models.Workspace( + workspace_id="w1", resources=[ - api_models_generated.Resource( - id="r2", name="r2", type=api_models.ResourceType.simulator + api_models.Resource( + workspace_id="w1", resource_id="r2", resource_name="r2", resource_type="simulator" ) ], ) diff --git a/test/test_job_persistence.py b/test/test_job_persistence.py index 32316fa..e5496aa 100644 --- a/test/test_job_persistence.py +++ b/test/test_job_persistence.py @@ -25,6 +25,7 @@ from qiskit.providers import JobStatus from qiskit_aqt_provider import persistence +from qiskit_aqt_provider.api_client import Resource from qiskit_aqt_provider.api_client import models as api_models from qiskit_aqt_provider.api_client import models_generated as api_models_generated from qiskit_aqt_provider.aqt_job import AQTJob @@ -97,7 +98,7 @@ def test_job_persistence_transaction_online_backend(httpx_mock: HTTPXMock, tmp_p # Set up a fake online resource token = str(uuid.uuid4()) provider = AQTProvider(token) - resource_id = api_models.ResourceId( + resource_id = Resource( workspace_id=str(uuid.uuid4()), resource_id=str(uuid.uuid4()), resource_name=str(uuid.uuid4()), diff --git a/test/test_provider.py b/test/test_provider.py index 280f493..bc5603c 100644 --- a/test/test_provider.py +++ b/test/test_provider.py @@ -17,11 +17,12 @@ import uuid from pathlib import Path from unittest import mock -from urllib.parse import urlparse +import httpx import pytest from pytest_httpx import HTTPXMock +from qiskit_aqt_provider.api_client import DEFAULT_PORTAL_URL from qiskit_aqt_provider.api_client import models as api_models from qiskit_aqt_provider.api_client import models_generated as api_models_generated from qiskit_aqt_provider.aqt_provider import OFFLINE_SIMULATORS, AQTProvider, NoTokenWarning @@ -32,26 +33,18 @@ def test_default_portal_url() -> None: with mock.patch.object(os, "environ", {}): aqt = AQTProvider("my-token") - result = urlparse(aqt.portal_url) - expected = urlparse(AQTProvider.DEFAULT_PORTAL_URL) - - assert result.scheme == expected.scheme - assert result.netloc == expected.netloc + assert aqt._portal_client.portal_url == DEFAULT_PORTAL_URL def test_portal_url_envvar(monkeypatch: pytest.MonkeyPatch) -> None: """Check that one can set the portal url via the environment variable.""" - env_url = "https://new-portal.aqt.eu" - assert env_url != AQTProvider.DEFAULT_PORTAL_URL - monkeypatch.setenv("AQT_PORTAL_URL", env_url) + env_url = httpx.URL("https://new-portal.aqt.eu") + assert env_url != DEFAULT_PORTAL_URL + monkeypatch.setenv("AQT_PORTAL_URL", str(env_url)) aqt = AQTProvider("my-token") - result = urlparse(aqt.portal_url) - expected = urlparse(env_url) - - assert result.scheme == expected.scheme - assert result.netloc == expected.netloc + assert aqt._portal_client.portal_url == env_url def test_access_token_argument() -> None: