lqcloud 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lqcloud/__init__.py +75 -0
- lqcloud/auth/__init__.py +5 -0
- lqcloud/auth/config.py +189 -0
- lqcloud/backend/__init__.py +1 -0
- lqcloud/backend/serialization.py +824 -0
- lqcloud/circuit/__init__.py +87 -0
- lqcloud/circuit/circuit.py +1349 -0
- lqcloud/circuit/classical_register.py +46 -0
- lqcloud/circuit/gates.py +907 -0
- lqcloud/circuit/parameter.py +182 -0
- lqcloud/circuit/pauli_op.py +25 -0
- lqcloud/circuit/quantum_register.py +25 -0
- lqcloud/exceptions.py +70 -0
- lqcloud/http_session.py +337 -0
- lqcloud/http_utils.py +164 -0
- lqcloud/job/__init__.py +7 -0
- lqcloud/job/job.py +855 -0
- lqcloud/job/result.py +1056 -0
- lqcloud/job/status.py +42 -0
- lqcloud/per_shot_config.py +85 -0
- lqcloud/primitives/__init__.py +149 -0
- lqcloud/primitives/simulator.py +326 -0
- lqcloud/provider/__init__.py +10 -0
- lqcloud/provider/cloud_provider.py +1719 -0
- lqcloud/py.typed +1 -0
- lqcloud/visualization/__init__.py +22 -0
- lqcloud/visualization/_draw_circuit.py +408 -0
- lqcloud/visualization/_plot_histogram.py +77 -0
- lqcloud/visualization/format_circuit.py +118 -0
- lqcloud/visualization/styles.py +15 -0
- lqcloud-0.4.2.dist-info/METADATA +204 -0
- lqcloud-0.4.2.dist-info/RECORD +35 -0
- lqcloud-0.4.2.dist-info/WHEEL +5 -0
- lqcloud-0.4.2.dist-info/licenses/LICENSE +201 -0
- lqcloud-0.4.2.dist-info/top_level.txt +1 -0
lqcloud/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""LQCloud SDK — Cloud Quantum Computing toolkit.
|
|
2
|
+
|
|
3
|
+
All commonly-used classes and functions are available at the top level::
|
|
4
|
+
|
|
5
|
+
from lqcloud import LQCloudProvider, QuantumCircuit, save_account, plot_histogram
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .provider import LQCloudProvider
|
|
9
|
+
from .circuit.circuit import QuantumCircuit
|
|
10
|
+
from .circuit.quantum_register import QuantumRegister
|
|
11
|
+
from .circuit.classical_register import ClassicalRegister
|
|
12
|
+
from .circuit.pauli_op import PauliOp
|
|
13
|
+
from .circuit.parameter import Parameter
|
|
14
|
+
from .auth.config import save_account, load_account
|
|
15
|
+
from .visualization import plot_histogram
|
|
16
|
+
from .job.status import JobStatus
|
|
17
|
+
from .job.result import BatchResult, Result
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
LQCloudError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
BackendNotFoundError,
|
|
22
|
+
CircuitError,
|
|
23
|
+
JobError,
|
|
24
|
+
JobTimeoutError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# 版本号统一从已安装包的元数据读取,单一来源在 ``pyproject.toml``,
|
|
28
|
+
# 避免出现 wheel 已升到新版本但这里硬编码字符串还停留在旧版本的情况。
|
|
29
|
+
try: # pragma: no cover - 标准库导入兜底
|
|
30
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
31
|
+
except ImportError: # Python <3.8 理论上不会走到,仅作保险
|
|
32
|
+
from importlib_metadata import ( # type: ignore[no-redef]
|
|
33
|
+
PackageNotFoundError,
|
|
34
|
+
version as _pkg_version,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
__version__ = _pkg_version("lqcloud")
|
|
39
|
+
except PackageNotFoundError:
|
|
40
|
+
# 直接从源码目录运行(未通过 pip 安装)时的兜底值,保证 ``import lqcloud``
|
|
41
|
+
# 不会因为读不到元数据而失败。
|
|
42
|
+
__version__ = "0.0.0+local"
|
|
43
|
+
|
|
44
|
+
del _pkg_version, PackageNotFoundError
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
# Provider / Backend
|
|
50
|
+
"LQCloudProvider",
|
|
51
|
+
# Circuit construction
|
|
52
|
+
"QuantumCircuit",
|
|
53
|
+
"QuantumRegister",
|
|
54
|
+
"ClassicalRegister",
|
|
55
|
+
"PauliOp",
|
|
56
|
+
"Parameter",
|
|
57
|
+
# Authentication
|
|
58
|
+
"save_account",
|
|
59
|
+
"load_account",
|
|
60
|
+
# Visualization
|
|
61
|
+
"plot_histogram",
|
|
62
|
+
# Job
|
|
63
|
+
"JobStatus",
|
|
64
|
+
"Result",
|
|
65
|
+
"BatchResult",
|
|
66
|
+
# Exceptions
|
|
67
|
+
"LQCloudError",
|
|
68
|
+
"AuthenticationError",
|
|
69
|
+
"BackendNotFoundError",
|
|
70
|
+
"CircuitError",
|
|
71
|
+
"JobError",
|
|
72
|
+
"JobTimeoutError",
|
|
73
|
+
# Meta
|
|
74
|
+
"__version__",
|
|
75
|
+
]
|
lqcloud/auth/__init__.py
ADDED
lqcloud/auth/config.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Authentication configuration — save, load, and interactive setup.
|
|
2
|
+
|
|
3
|
+
Configuration is stored in ``~/.lqcloud/config.json``. The SDK checks
|
|
4
|
+
environment variables first, then the config file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("lqcloud")
|
|
16
|
+
|
|
17
|
+
# Default server URL (public endpoint, no internal IPs)
|
|
18
|
+
DEFAULT_URL = "https://cloud.logicalqubit.com"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_config_dir() -> Path:
|
|
22
|
+
"""Return the configuration directory, with fallback for sandboxed envs."""
|
|
23
|
+
try:
|
|
24
|
+
return Path.home() / ".lqcloud"
|
|
25
|
+
except RuntimeError:
|
|
26
|
+
fallback = Path(os.environ.get("TEMP", os.environ.get("TMP", "."))) / ".lqcloud"
|
|
27
|
+
return fallback
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEFAULT_CONFIG_DIR = _get_config_dir()
|
|
31
|
+
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_account(
|
|
35
|
+
api_key: Optional[str] = None,
|
|
36
|
+
token: Optional[str] = None,
|
|
37
|
+
url: str = DEFAULT_URL,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Persist authentication credentials to the local config file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_key: API Key issued by the LQCloud platform.
|
|
43
|
+
token: Alternative bearer token.
|
|
44
|
+
url: Backend URL (default: ``https://cloud.logicalqubit.com``).
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If neither ``api_key`` nor ``token`` is provided, or if
|
|
48
|
+
``url`` is empty / does not use the ``http://`` or ``https://``
|
|
49
|
+
scheme.
|
|
50
|
+
"""
|
|
51
|
+
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
52
|
+
token = token.strip() if isinstance(token, str) else token
|
|
53
|
+
url = url.strip() if isinstance(url, str) else url
|
|
54
|
+
|
|
55
|
+
if not api_key and not token:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"At least one of api_key / token must be provided and non-empty."
|
|
58
|
+
)
|
|
59
|
+
if not url:
|
|
60
|
+
raise ValueError("url must be a non-empty string.")
|
|
61
|
+
if not (url.startswith("http://") or url.startswith("https://")):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"url must start with 'http://' or 'https://' "
|
|
64
|
+
f"(got: {url!r})."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
# 限制配置目录权限,防止其他用户枚举/读取凭证文件
|
|
69
|
+
try:
|
|
70
|
+
if DEFAULT_CONFIG_DIR.stat().st_mode != (stat.S_IFDIR | 0o700):
|
|
71
|
+
os.chmod(DEFAULT_CONFIG_DIR, 0o700)
|
|
72
|
+
except OSError:
|
|
73
|
+
logger.warning("Unable to set permissions on %s", DEFAULT_CONFIG_DIR)
|
|
74
|
+
|
|
75
|
+
data = {"api_key": api_key, "token": token, "url": url}
|
|
76
|
+
|
|
77
|
+
with open(DEFAULT_CONFIG_FILE, "w") as f:
|
|
78
|
+
json.dump(data, f, indent=4)
|
|
79
|
+
|
|
80
|
+
# 限制配置文件權限,防止其他用戶讀取 API Key / Token
|
|
81
|
+
try:
|
|
82
|
+
os.chmod(DEFAULT_CONFIG_FILE, 0o600)
|
|
83
|
+
except OSError:
|
|
84
|
+
logger.warning("Unable to set permissions on %s", DEFAULT_CONFIG_FILE)
|
|
85
|
+
|
|
86
|
+
logger.info("Account saved to %s", DEFAULT_CONFIG_FILE)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_account(interactive: bool = True) -> Optional[dict]:
|
|
90
|
+
"""Load authentication configuration.
|
|
91
|
+
|
|
92
|
+
Priority: **environment variables** > **config file** > **interactive setup**.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
interactive: When ``True`` (default) and no credentials are found
|
|
96
|
+
in the environment or config file, fall back to
|
|
97
|
+
:func:`interactive_setup`. When ``False``, return ``None``
|
|
98
|
+
instead — useful in non-interactive environments (CI, Docker,
|
|
99
|
+
Jupyter headless, scheduled tasks).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A dict with keys ``api_key``, ``token``, ``url``, or ``None`` if
|
|
103
|
+
nothing is available and interactive setup is disabled or cancelled.
|
|
104
|
+
"""
|
|
105
|
+
# 1. Environment variables
|
|
106
|
+
env_api_key = os.getenv("LQCLOUD_API_KEY")
|
|
107
|
+
env_token = os.getenv("LQCLOUD_TOKEN")
|
|
108
|
+
env_url = os.getenv("LQCLOUD_URL")
|
|
109
|
+
|
|
110
|
+
if env_api_key or env_token:
|
|
111
|
+
env_dict: dict = {}
|
|
112
|
+
if env_api_key:
|
|
113
|
+
env_dict["api_key"] = env_api_key
|
|
114
|
+
if env_token:
|
|
115
|
+
env_dict["token"] = env_token
|
|
116
|
+
env_dict["url"] = env_url or DEFAULT_URL
|
|
117
|
+
return env_dict
|
|
118
|
+
|
|
119
|
+
# 2. Config file
|
|
120
|
+
if DEFAULT_CONFIG_FILE.exists():
|
|
121
|
+
try:
|
|
122
|
+
with open(DEFAULT_CONFIG_FILE, "r") as f:
|
|
123
|
+
return json.load(f)
|
|
124
|
+
except json.JSONDecodeError:
|
|
125
|
+
pass # corrupted file — fall through
|
|
126
|
+
|
|
127
|
+
# 3. Interactive setup (optional)
|
|
128
|
+
if not interactive:
|
|
129
|
+
return None
|
|
130
|
+
return interactive_setup()
|
|
131
|
+
|
|
132
|
+
import getpass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _stdin_is_interactive() -> bool:
|
|
136
|
+
"""Return ``True`` only when stdin can actually be prompted.
|
|
137
|
+
|
|
138
|
+
In non-interactive environments — CI, Windows services, Docker, Jupyter
|
|
139
|
+
headless, scheduled tasks, or under pytest's output capture — stdin is not
|
|
140
|
+
a TTY and reading it raises ``OSError`` (or hangs). Detect that up front so
|
|
141
|
+
:func:`interactive_setup` can skip prompting instead of crashing.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
return bool(sys.stdin) and sys.stdin.isatty()
|
|
145
|
+
except (ValueError, OSError, AttributeError):
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def interactive_setup() -> Optional[dict]:
|
|
150
|
+
"""Prompt the user for API key and server URL, then save.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A dict with ``api_key`` and ``url``, or ``None`` if the user
|
|
154
|
+
cancels via ``Ctrl-C`` / ``Ctrl-D``, or if stdin is not interactive.
|
|
155
|
+
"""
|
|
156
|
+
# Never read stdin in a non-interactive environment (CI / service / headless
|
|
157
|
+
# / pytest capture); doing so raises OSError or blocks forever.
|
|
158
|
+
if not _stdin_is_interactive():
|
|
159
|
+
logger.info(
|
|
160
|
+
"Non-interactive stdin detected; skipping credential prompt. "
|
|
161
|
+
"Set LQCLOUD_API_KEY, call save_account(...), or pass api_key/token."
|
|
162
|
+
)
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
print("=" * 70)
|
|
167
|
+
print("🔐 LQCloud Account Setup")
|
|
168
|
+
print("=" * 70)
|
|
169
|
+
print("First-time configuration required.")
|
|
170
|
+
print(f"Credentials will be saved to {DEFAULT_CONFIG_FILE}")
|
|
171
|
+
print("-" * 70)
|
|
172
|
+
|
|
173
|
+
api_key = getpass.getpass("Enter API Key: ").strip()
|
|
174
|
+
while not api_key:
|
|
175
|
+
print("❌ API Key cannot be empty.")
|
|
176
|
+
api_key = getpass.getpass("Enter API Key: ").strip()
|
|
177
|
+
|
|
178
|
+
url_input = input(f"Enter server URL [default: {DEFAULT_URL}]: ").strip()
|
|
179
|
+
url = url_input if url_input else DEFAULT_URL
|
|
180
|
+
|
|
181
|
+
print("-" * 70)
|
|
182
|
+
save_account(api_key=api_key, url=url)
|
|
183
|
+
print("✅ Configuration complete!")
|
|
184
|
+
print("=" * 70)
|
|
185
|
+
except (KeyboardInterrupt, EOFError):
|
|
186
|
+
print("\nSetup cancelled.")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
return {"api_key": api_key, "url": url}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# lqcloud backend module
|