Skip to content

Commit

Permalink
Split-off the API client resources in an isolated sub-package (#198)
Browse files Browse the repository at this point in the history
* Move API data structures to separate sub-package

* Setup tach. Enfore isolation of api_client

---------

Co-authored-by: Wilfried Huss <84843123+wilfried-huss@users.noreply.github.com>
  • Loading branch information
airwoodix and wilfried-huss authored Nov 20, 2024
1 parent d9bfbbe commit 96f77a3
Show file tree
Hide file tree
Showing 25 changed files with 324 additions and 45 deletions.
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ repos:
- id: interrogate
args: [-v, qiskit_aqt_provider, test]
pass_filenames: false # needed if excluding files with pyproject.toml or setup.cfg
- repo: https://github.com/airwoodix/tach-pre-commit
rev: "v0.14.3-external"
hooks:
- id: tach
- id: tach-external
args: ["-e", "test/"]
- repo: local
hooks:
- id: check-api-models
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Fix source installation problem caused by removed debugpy 1.8.3 package (#188)
* User guide: add section on direct-access resources (#191)
* Split Qiskit-agnostic client resources to separate sub-package (#198)

## qiskit-aqt-provider v1.8.1

Expand Down
178 changes: 176 additions & 2 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pytest_qiskit_aqt = "qiskit_aqt_provider.test.fixtures"
[tool.poetry.dependencies]
python = ">=3.9,<3.13"

annotated-types = ">=0.7.0"
httpx = ">=0.24.0"
platformdirs = ">=3"
pydantic = ">=2.5.0"
Expand Down Expand Up @@ -100,6 +101,7 @@ rich = "^13.5.3"
ruff = "^0.7.0"
sphinx = ">=7,<7.3"
sphinx-toolbox = "^3.5.0"
tach = "^0.14.3"
tomlkit = "^0.13.2"
typer = "^0.13.0"
types-requests = "^2.28.11"
Expand Down Expand Up @@ -191,7 +193,7 @@ lint.ignore = [
lint.per-file-ignores."examples/*.py" = [
"T201", # allow prints
]
lint.per-file-ignores."qiskit_aqt_provider/api_models_generated.py" = [
lint.per-file-ignores."qiskit_aqt_provider/api_client/models_generated.py" = [
"D100", # undocumented-public-module
"D101", # undocumented-public-class
"E501", # line-too-long
Expand Down Expand Up @@ -257,7 +259,7 @@ ignore-module = true
ignore-nested-functions = true
ignore-magic = true
exclude = [
"qiskit_aqt_provider/api_models_generated.py",
"qiskit_aqt_provider/api_client/models_generated.py",
]
fail-under = 100

Expand Down Expand Up @@ -313,13 +315,23 @@ shell = "interrogate -v qiskit_aqt_provider test"
[tool.poe.tasks.spellcheck]
shell = "typos ."

[tool.poe.tasks.check_internal_dependencies]
shell = "tach check"

[tool.poe.tasks.check_external_dependencies]
# Exclude the test module because tach doesn't collect
# dependencies outside the main group.
shell = "tach check-external -e test/"

[tool.poe.tasks]
lint = [
"check_pre_commit_consistency",
"check_api_models",
"docstring_coverage",
"ruff_check",
"spellcheck",
"check_internal_dependencies",
"check_external_dependencies",
]
format_check = [
"python_format_check",
Expand Down
9 changes: 9 additions & 0 deletions qiskit_aqt_provider/api_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# (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.
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,27 @@

"""Thin convenience wrappers around generated API models."""

import importlib.metadata
import platform
import re
from re import Pattern
from typing import Any, Literal, Optional, Union
from typing import Any, Final, Literal, Optional, Union
from uuid import UUID

import httpx
import pydantic as pdt
from qiskit.providers.exceptions import JobError
from typing_extensions import Self, TypeAlias

from qiskit_aqt_provider import api_models_generated as api_models
from qiskit_aqt_provider.api_models_generated import (
from . import models_generated as api_models
from .models_generated import (
Circuit,
OperationModel,
QuantumCircuit,
QuantumCircuits,
SubmitJobRequest,
)
from qiskit_aqt_provider.api_models_generated import Type as ResourceType
from qiskit_aqt_provider.versions import USER_AGENT
from .models_generated import Type as ResourceType

__all__ = [
"Circuit",
Expand All @@ -50,14 +51,27 @@ class UnknownJobError(JobError):
"""An unknown job was requested from the AQT cloud portal."""


def http_client(*, base_url: str, token: str) -> httpx.Client:
PACKAGE_VERSION: Final = importlib.metadata.version("qiskit-aqt-provider")
USER_AGENT: Final = " ".join(
[
f"aqt-api-client/{PACKAGE_VERSION}",
f"({platform.system()}; {platform.python_implementation()}/{platform.python_version()})",
]
)


def http_client(
*, base_url: str, token: str, user_agent_extra: Optional[str] = None
) -> httpx.Client:
"""A pre-configured httpx Client.
Args:
base_url: base URL of the server
token: access token for the remote service.
user_agent_extra: optional extra data to add to the user-agent string.
"""
headers = {"User-Agent": USER_AGENT}
user_agent_extra = f" {user_agent_extra}" if user_agent_extra else ""
headers = {"User-Agent": USER_AGENT + user_agent_extra}
if token:
headers["Authorization"] = f"Bearer {token}"

Expand Down
File renamed without changes.
File renamed without changes.
5 changes: 3 additions & 2 deletions qiskit_aqt_provider/aqt_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
from tqdm import tqdm
from typing_extensions import Self, TypeAlias, assert_never

from qiskit_aqt_provider import api_models_generated, persistence
from qiskit_aqt_provider.api_models_direct import JobResultError
from qiskit_aqt_provider import persistence
from qiskit_aqt_provider.api_client import models_generated as api_models_generated
from qiskit_aqt_provider.api_client.models_direct import JobResultError
from qiskit_aqt_provider.aqt_options import AQTOptions
from qiskit_aqt_provider.circuit_to_aqt import circuits_to_aqt_job

Expand Down
7 changes: 5 additions & 2 deletions qiskit_aqt_provider/aqt_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
from tabulate import tabulate
from typing_extensions import TypeAlias, override

from qiskit_aqt_provider import api_models
from qiskit_aqt_provider.api_client import models as api_models
from qiskit_aqt_provider.aqt_resource import (
AQTDirectAccessResource,
AQTResource,
OfflineSimulatorResource,
)
from qiskit_aqt_provider.versions import USER_AGENT_EXTRA

StrPath: TypeAlias = Union[str, Path]

Expand Down Expand Up @@ -201,7 +202,9 @@ def __init__(
@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)
return api_models.http_client(
base_url=self.portal_url, token=self.access_token, user_agent_extra=USER_AGENT_EXTRA
)

def backends(
self,
Expand Down
8 changes: 6 additions & 2 deletions qiskit_aqt_provider/aqt_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@
from qiskit_aer import AerJob, AerSimulator, noise
from typing_extensions import override

from qiskit_aqt_provider import api_models, api_models_direct
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
from qiskit_aqt_provider.aqt_options import AQTDirectAccessOptions, AQTOptions
from qiskit_aqt_provider.circuit_to_aqt import aqt_to_qiskit_circuit
from qiskit_aqt_provider.versions import USER_AGENT_EXTRA

if TYPE_CHECKING: # pragma: no cover
from qiskit_aqt_provider.aqt_provider import AQTProvider
Expand Down Expand Up @@ -306,7 +308,9 @@ def __init__(
options_type=AQTDirectAccessOptions,
)

self._http_client = api_models.http_client(base_url=base_url, token=provider.access_token)
self._http_client = api_models.http_client(
base_url=base_url, token=provider.access_token, user_agent_extra=USER_AGENT_EXTRA
)

def run(
self, circuits: Union[QuantumCircuit, list[QuantumCircuit]], **options: Any
Expand Down
3 changes: 2 additions & 1 deletion qiskit_aqt_provider/circuit_to_aqt.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from qiskit import QuantumCircuit
from typing_extensions import assert_never

from qiskit_aqt_provider import api_models, api_models_generated
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 qiskit_to_aqt_circuit(circuit: QuantumCircuit) -> api_models.Circuit:
Expand Down
2 changes: 1 addition & 1 deletion qiskit_aqt_provider/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from qiskit.circuit import QuantumCircuit
from typing_extensions import Self

from qiskit_aqt_provider.api_models import ResourceId
from qiskit_aqt_provider.api_client.models import ResourceId
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
Expand Down
3 changes: 2 additions & 1 deletion qiskit_aqt_provider/test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
from qiskit_aer import AerSimulator
from typing_extensions import override

from qiskit_aqt_provider import api_models, api_models_direct
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
from qiskit_aqt_provider.aqt_provider import AQTProvider
from qiskit_aqt_provider.aqt_resource import (
Expand Down
2 changes: 1 addition & 1 deletion qiskit_aqt_provider/test/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from qiskit import QuantumCircuit
from typing_extensions import assert_never, override

from qiskit_aqt_provider import api_models
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
from qiskit_aqt_provider.aqt_resource import AQTDirectAccessResource, AQTResource
Expand Down
9 changes: 1 addition & 8 deletions qiskit_aqt_provider/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,11 @@
# that they have been altered from the originals.

import importlib.metadata
import platform
from typing import Final

QISKIT_VERSION: Final = importlib.metadata.version("qiskit")
QISKIT_AQT_PROVIDER_VERSION: Final = importlib.metadata.version("qiskit-aqt-provider")

__version__: Final = QISKIT_AQT_PROVIDER_VERSION

USER_AGENT: Final = " ".join(
[
f"qiskit-aqt-provider/{QISKIT_AQT_PROVIDER_VERSION}",
f"({platform.system()}; {platform.python_implementation()}/{platform.python_version()})",
f"qiskit/{QISKIT_VERSION}",
]
)
USER_AGENT_EXTRA: Final = f"qiskit/{QISKIT_VERSION}"
2 changes: 1 addition & 1 deletion scripts/api_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def default_schema_path() -> Path:

def default_models_path() -> Path:
"""Default destination of generated Pydantic models."""
return repo_root() / "qiskit_aqt_provider" / "api_models_generated.py"
return repo_root() / "qiskit_aqt_provider" / "api_client" / "models_generated.py"


def generate_models(schema_path: Path, dest_path: Path, *, ruff_lint_extra_args: str = "") -> None:
Expand Down
1 change: 1 addition & 0 deletions scripts/check_pre_commit_consistency.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
readonly SCRIPT_DIR

# Tools to check.
# FIXME: add tach when using upstream tag again
TOOLS=(ruff typos pyproject-fmt interrogate)
readonly TOOLS

Expand Down
54 changes: 54 additions & 0 deletions tach.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
exclude = [
".*__pycache__",
"docs",
]
exact = true
ignore_type_checking_imports = true
forbid_circular_dependencies = true

source_roots = [
".",
]

[[modules]]
path = "qiskit_aqt_provider"
depends_on = [
{ path = "qiskit_aqt_provider.api_client" },
]

[[modules]]
path = "qiskit_aqt_provider.api_client"
depends_on = []

[[modules]]
path = "test"
depends_on = [
{ path = "qiskit_aqt_provider" },
{ path = "qiskit_aqt_provider.api_client" },
]

[[modules]]
path = "examples"
depends_on = [
{ path = "qiskit_aqt_provider" },
]
strict = true

[external]
exclude = [
"python",
# if dotenv is not installed (like e.g. in the pre-commit hook's environment)
# tach cannot know that this is the distribution for dotenv
# https://github.com/gauge-sh/tach/issues/414
"python_dotenv",
"dotenv",
# pydantic-core always comes with pydantic
"pydantic_core",
# testing dependencies:
"pytest",
"pytest_sugar",
"pytest_mock",
# example dependencies:
"qiskit_algorithms",
"qiskit_optimization",
]
Empty file added test/api_client/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion test/test_api_models.py → test/api_client/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

from qiskit_aqt_provider import api_models, api_models_generated
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_filter_by_workspace() -> None:
Expand Down
2 changes: 1 addition & 1 deletion test/test_circuit_to_aqt.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from pydantic import ValidationError
from qiskit import QuantumCircuit

from qiskit_aqt_provider import api_models
from qiskit_aqt_provider.api_client import models as api_models
from qiskit_aqt_provider.aqt_resource import AQTResource
from qiskit_aqt_provider.circuit_to_aqt import (
aqt_to_qiskit_circuit,
Expand Down
4 changes: 3 additions & 1 deletion test/test_job_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
from pytest_mock import MockerFixture
from qiskit.providers import JobStatus

from qiskit_aqt_provider import api_models, api_models_generated, persistence
from qiskit_aqt_provider import persistence
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
from qiskit_aqt_provider.aqt_options import AQTOptions
from qiskit_aqt_provider.aqt_provider import AQTProvider
Expand Down
3 changes: 2 additions & 1 deletion test/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
import pytest
from pytest_httpx import HTTPXMock

from qiskit_aqt_provider import api_models, api_models_generated
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


Expand Down
Loading

0 comments on commit 96f77a3

Please sign in to comment.