fujitsu-quantum 1.4.2__tar.gz
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.
- fujitsu_quantum-1.4.2/PKG-INFO +24 -0
- fujitsu_quantum-1.4.2/README.md +1 -0
- fujitsu_quantum-1.4.2/pyproject.toml +69 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/__init__.py +1 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/auth.py +189 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/aws_utils.py +64 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/cli/fujitsu_quantum_login.py +150 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/concurrent/__init__.py +1 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/concurrent/futures.py +65 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/config.py +55 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/devices.py +438 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/logging.py +26 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/openfermion/__init__.py +1 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/openfermion/operators.py +32 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/openfermion/qiskit_operators.py +32 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/__init__.py +13 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/backends/__init__.py +12 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/backends/fujitsu_backend.py +111 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/backends/fujitsu_qpu_backend.py +189 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/backends/fujitsu_simulator_backend.py +89 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/fujitsu_job.py +327 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/fujitsu_provider.py +24 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/library/__init__.py +7 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/library/rzx90.py +159 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/primitives/__init__.py +7 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qiskit/primitives/fujitsu_estimator.py +235 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qulacs/__init__.py +1 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qulacs/operators.py +22 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/plugins/qulacs/qasm.py +88 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/requests.py +157 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/results.py +99 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/tasks.py +422 -0
- fujitsu_quantum-1.4.2/src/fujitsu_quantum/warnings.py +4 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fujitsu-quantum
|
|
3
|
+
Version: 1.4.2
|
|
4
|
+
Summary: Fujitsu Quantum Cloud SDK
|
|
5
|
+
Author: Fujitsu Limited
|
|
6
|
+
Requires-Python: >=3.8,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Provides-Extra: qiskit
|
|
14
|
+
Requires-Dist: boto3 (>=1.34.58,<2.0.0)
|
|
15
|
+
Requires-Dist: filelock (>=3.13.1,<4.0.0)
|
|
16
|
+
Requires-Dist: pycognito (>=2024.2.0,<2025.0.0)
|
|
17
|
+
Requires-Dist: pyjson5 (>=1.6.6,<2.0.0)
|
|
18
|
+
Requires-Dist: qiskit (>=0.45,<0.46) ; extra == "qiskit"
|
|
19
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
20
|
+
Requires-Dist: typing-extensions (>=4.8.0,<5.0.0) ; python_version < "3.11"
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
An official SDK for Fujitsu Quantum Cloud.
|
|
24
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
An official SDK for Fujitsu Quantum Cloud.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "fujitsu-quantum"
|
|
3
|
+
version = "1.4.2"
|
|
4
|
+
description = "Fujitsu Quantum Cloud SDK"
|
|
5
|
+
authors = ["Fujitsu Limited"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "fujitsu_quantum", from = "src"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.scripts]
|
|
10
|
+
fujitsu-quantum = 'fujitsu_quantum.cli.fujitsu_quantum_login:main'
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = "^3.8"
|
|
14
|
+
requests = "^2.32.3"
|
|
15
|
+
typing-extensions = { version = "^4.8.0", python = "<3.11" }
|
|
16
|
+
qiskit = { version = "^0.45", optional = true }
|
|
17
|
+
pyjson5 = "^1.6.6"
|
|
18
|
+
boto3 = "^1.34.58"
|
|
19
|
+
pycognito = "^2024.2.0"
|
|
20
|
+
filelock = "^3.13.1"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.group.dev.dependencies]
|
|
23
|
+
isort = "^5.12.0"
|
|
24
|
+
flake8 = "^5.0.4"
|
|
25
|
+
autoflake = "^1.7.8"
|
|
26
|
+
autopep8 = "^2.0.0"
|
|
27
|
+
mypy = "^1.4.1"
|
|
28
|
+
types-requests = "^2.31.0.2"
|
|
29
|
+
pytest = "^7.4.0"
|
|
30
|
+
qiskit-qasm3-import = "^0.4.1"
|
|
31
|
+
qulacs = "^0.6.4.1"
|
|
32
|
+
openfermionpyscf = "^0.5"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.extras]
|
|
35
|
+
qiskit = ["qiskit"]
|
|
36
|
+
|
|
37
|
+
[tool.isort]
|
|
38
|
+
line_length = 120
|
|
39
|
+
|
|
40
|
+
[tool.autopep8]
|
|
41
|
+
max_line_length = 120
|
|
42
|
+
recursive = true
|
|
43
|
+
ignore = ["E501"]
|
|
44
|
+
|
|
45
|
+
[tool.mypy]
|
|
46
|
+
exclude = [
|
|
47
|
+
".venv",
|
|
48
|
+
".git",
|
|
49
|
+
]
|
|
50
|
+
ignore_missing_imports = true
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = [
|
|
54
|
+
"tests",
|
|
55
|
+
]
|
|
56
|
+
filterwarnings = [
|
|
57
|
+
"ignore::DeprecationWarning:qiskit.utils.algorithm_globals",
|
|
58
|
+
"ignore::PendingDeprecationWarning:qiskit.utils.deprecation",
|
|
59
|
+
"ignore::PendingDeprecationWarning:qiskit.visualization.circuit.matplotlib"
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report]
|
|
63
|
+
exclude_lines = [
|
|
64
|
+
"if TYPE_CHECKING:",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[build-system]
|
|
68
|
+
requires = ["poetry-core"]
|
|
69
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from botocore.exceptions import ClientError
|
|
15
|
+
from filelock import BaseFileLock, FileLock
|
|
16
|
+
from requests.auth import AuthBase
|
|
17
|
+
|
|
18
|
+
from fujitsu_quantum import aws_utils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FjqAuthError(Exception):
|
|
22
|
+
@property
|
|
23
|
+
def msg(self) -> str:
|
|
24
|
+
return self.args[0]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FjqAuth(ABC, AuthBase):
|
|
28
|
+
CREDENTIALS_DIR: str = ".fujitsu-quantum/"
|
|
29
|
+
CREDENTIALS_FILE: str = "credentials.json"
|
|
30
|
+
CREDENTIALS_FILEPATH: Path = Path(Path.home() / CREDENTIALS_DIR / CREDENTIALS_FILE)
|
|
31
|
+
CREDENTIALS_FILE_LOCK_PATH: Path = CREDENTIALS_FILEPATH.with_suffix(CREDENTIALS_FILEPATH.suffix + ".lock")
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._load_credentials()
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def store_credentials(client_id: str,
|
|
38
|
+
username: str,
|
|
39
|
+
access_token: str,
|
|
40
|
+
access_token_expiration_time: datetime,
|
|
41
|
+
refresh_token: str,
|
|
42
|
+
refresh_token_expiration_time: datetime,
|
|
43
|
+
device_key: Optional[str]) -> None:
|
|
44
|
+
|
|
45
|
+
if not os.path.exists(Path.home() / FjqAuth.CREDENTIALS_DIR):
|
|
46
|
+
os.makedirs(Path.home() / FjqAuth.CREDENTIALS_DIR)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
lock = FileLock(FjqAuth.CREDENTIALS_FILE_LOCK_PATH)
|
|
50
|
+
with lock:
|
|
51
|
+
with os.fdopen(
|
|
52
|
+
os.open(FjqAuth.CREDENTIALS_FILEPATH, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w"
|
|
53
|
+
) as cred_json_file:
|
|
54
|
+
cred_json = {
|
|
55
|
+
"ClientId": client_id,
|
|
56
|
+
"Username": username,
|
|
57
|
+
"AccessToken": access_token,
|
|
58
|
+
"AccessTokenExpirationTime": access_token_expiration_time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
59
|
+
"RefreshToken": refresh_token,
|
|
60
|
+
"RefreshTokenExpirationTime": refresh_token_expiration_time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if device_key is not None:
|
|
64
|
+
cred_json["DeviceKey"] = device_key
|
|
65
|
+
|
|
66
|
+
cred_json_file.write(json.dumps(cred_json, indent=4))
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
raise FjqAuthError(f"Unable to open credentials file lock: {FjqAuth.CREDENTIALS_FILE_LOCK_PATH}")
|
|
69
|
+
|
|
70
|
+
def update_access_token_in_credential_file(self, parent_lock: BaseFileLock = None) -> None:
|
|
71
|
+
cred_json = self._read_credential_file()
|
|
72
|
+
cred_json["AccessToken"] = self._access_token
|
|
73
|
+
try:
|
|
74
|
+
lock = parent_lock
|
|
75
|
+
if lock is None:
|
|
76
|
+
lock = FileLock(FjqAuth.CREDENTIALS_FILE_LOCK_PATH)
|
|
77
|
+
|
|
78
|
+
with lock:
|
|
79
|
+
with open(self.CREDENTIALS_FILEPATH, "w") as cred_json_file:
|
|
80
|
+
cred_json_file.write(json.dumps(cred_json, indent=4))
|
|
81
|
+
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
raise FjqAuthError(f"Unable to open credentials file lock: {FjqAuth.CREDENTIALS_FILE_LOCK_PATH}")
|
|
84
|
+
|
|
85
|
+
def _read_credential_file(self, parent_lock: BaseFileLock = None) -> Dict[str, str]:
|
|
86
|
+
try:
|
|
87
|
+
lock = parent_lock
|
|
88
|
+
if lock is None:
|
|
89
|
+
lock = FileLock(FjqAuth.CREDENTIALS_FILE_LOCK_PATH)
|
|
90
|
+
|
|
91
|
+
with lock:
|
|
92
|
+
try:
|
|
93
|
+
with open(self.CREDENTIALS_FILEPATH, 'r') as cred_json_file:
|
|
94
|
+
return json.load(cred_json_file)
|
|
95
|
+
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
raise FjqAuthError(
|
|
98
|
+
"Credentials not found."
|
|
99
|
+
" Please run `fujitsu-quantum login` to configure credentials.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
raise FjqAuthError("The credential data is corrupted."
|
|
104
|
+
" Please run `fujitsu-quantum login` to configure credentials.")
|
|
105
|
+
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
raise FjqAuthError(f"Unable to open credentials file lock: {FjqAuth.CREDENTIALS_FILE_LOCK_PATH}")
|
|
108
|
+
|
|
109
|
+
def _load_credentials(self, parent_lock: BaseFileLock = None) -> None:
|
|
110
|
+
cred_json = self._read_credential_file(parent_lock)
|
|
111
|
+
try:
|
|
112
|
+
self._client_id: str = cred_json["ClientId"]
|
|
113
|
+
self._username: str = cred_json["Username"]
|
|
114
|
+
self._access_token: str = cred_json["AccessToken"]
|
|
115
|
+
self._access_token_expiration_time: str = cred_json["AccessTokenExpirationTime"]
|
|
116
|
+
self._refresh_token: str = cred_json["RefreshToken"]
|
|
117
|
+
self.refresh_token_expiration_time: str = cred_json["RefreshTokenExpirationTime"]
|
|
118
|
+
self._device_key: Optional[str] = cred_json.get("DeviceKey", None)
|
|
119
|
+
except KeyError:
|
|
120
|
+
raise FjqAuthError("The credentials are outdated."
|
|
121
|
+
" Please run `fujitsu-quantum login` to update the credentials.")
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def refresh_auth(self, force: bool = False) -> None:
|
|
125
|
+
raise NotImplementedError()
|
|
126
|
+
|
|
127
|
+
def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
|
|
128
|
+
r.headers["authorization"] = "Bearer " + self._access_token
|
|
129
|
+
return r
|
|
130
|
+
|
|
131
|
+
def update_authorization_header(self, r: requests.PreparedRequest):
|
|
132
|
+
r.headers["authorization"] = "Bearer " + self._access_token
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class FjqAuthBoto3(FjqAuth):
|
|
136
|
+
# TODO some error handling
|
|
137
|
+
def refresh_auth(self, force: bool = False) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Refreshes the access token.
|
|
140
|
+
If there is still time before the access token expires, this method does not refresh the access token to avoid
|
|
141
|
+
excessive token refresh requests.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
lock = FileLock(FjqAuth.CREDENTIALS_FILE_LOCK_PATH)
|
|
145
|
+
with lock:
|
|
146
|
+
if not force:
|
|
147
|
+
# Because other threads or processes may have updated the credential file, it should reload the file.
|
|
148
|
+
self._load_credentials(lock)
|
|
149
|
+
expiration_time = datetime.strptime(self._access_token_expiration_time, "%Y-%m-%d %H:%M:%S")
|
|
150
|
+
if expiration_time - datetime.now() > timedelta(minutes=10):
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
client = aws_utils.get_client("cognito-idp", region_name="ap-northeast-1")
|
|
154
|
+
|
|
155
|
+
new_hmac = hmac.new(bytes(self._username + self._client_id, "utf-8"),
|
|
156
|
+
digestmod=hashlib.sha256).digest()
|
|
157
|
+
secret_hash = base64.b64encode(new_hmac).decode()
|
|
158
|
+
|
|
159
|
+
auth_params = {
|
|
160
|
+
"USERNAME": self._username,
|
|
161
|
+
"SECRET_HASH": secret_hash,
|
|
162
|
+
"REFRESH_TOKEN": self._refresh_token,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if self._device_key is not None:
|
|
166
|
+
auth_params["DEVICE_KEY"] = self._device_key
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
response = client.initiate_auth(
|
|
170
|
+
ClientId=self._client_id,
|
|
171
|
+
AuthFlow="REFRESH_TOKEN_AUTH",
|
|
172
|
+
AuthParameters=auth_params,
|
|
173
|
+
)
|
|
174
|
+
self._access_token = response["AuthenticationResult"]["AccessToken"]
|
|
175
|
+
except ClientError as e:
|
|
176
|
+
if e.response["Error"]["Code"] == 'NotAuthorizedException':
|
|
177
|
+
raise FjqAuthError('The credentials have expired.'
|
|
178
|
+
' Please run `fujitsu-quantum login` to update the credentials.')
|
|
179
|
+
else:
|
|
180
|
+
raise e
|
|
181
|
+
|
|
182
|
+
self.update_access_token_in_credential_file(lock)
|
|
183
|
+
|
|
184
|
+
except FileNotFoundError:
|
|
185
|
+
raise FjqAuthError(f"Unable to open credentials file lock: {FjqAuth.CREDENTIALS_FILE_LOCK_PATH}")
|
|
186
|
+
|
|
187
|
+
def logout(self) -> None:
|
|
188
|
+
client = aws_utils.get_client("cognito-idp", region_name="ap-northeast-1")
|
|
189
|
+
client.revoke_token(Token=self._refresh_token, ClientId=self._client_id)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
from botocore.client import BaseClient
|
|
8
|
+
from botocore.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_aws_link_local_address_to_no_proxy():
|
|
12
|
+
# AWS SDK tries to connect http://169.254.169.254/** to retrieve metadata.
|
|
13
|
+
# 169.254.169.254 is a link-local address and must be accessed with no proxies.
|
|
14
|
+
# https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-proxy.html#cli-configure-proxy-ec2
|
|
15
|
+
aws_link_local_address = '169.254.169.254'
|
|
16
|
+
|
|
17
|
+
original_no_proxy = None
|
|
18
|
+
env_name_is_lowercase = True
|
|
19
|
+
if 'no_proxy' in os.environ:
|
|
20
|
+
original_no_proxy = os.environ['no_proxy']
|
|
21
|
+
elif 'NO_PROXY' in os.environ:
|
|
22
|
+
original_no_proxy = os.environ['NO_PROXY']
|
|
23
|
+
env_name_is_lowercase = False
|
|
24
|
+
|
|
25
|
+
if original_no_proxy is None:
|
|
26
|
+
os.environ['no_proxy'] = aws_link_local_address
|
|
27
|
+
else:
|
|
28
|
+
no_proxies = [address.strip() for address in original_no_proxy.split(',')]
|
|
29
|
+
if aws_link_local_address not in no_proxies:
|
|
30
|
+
if env_name_is_lowercase:
|
|
31
|
+
os.environ['no_proxy'] = f'{original_no_proxy},{aws_link_local_address}'
|
|
32
|
+
else:
|
|
33
|
+
os.environ['NO_PROXY'] = f'{original_no_proxy},{aws_link_local_address}'
|
|
34
|
+
|
|
35
|
+
return {'original_no_proxy': original_no_proxy, 'env_name_is_lowercase': env_name_is_lowercase}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def restore_no_proxy(original_no_proxy_config: Dict[str, Any]) -> None:
|
|
39
|
+
original_no_proxy = original_no_proxy_config['original_no_proxy']
|
|
40
|
+
env_name_is_lowercase = original_no_proxy_config['env_name_is_lowercase']
|
|
41
|
+
if original_no_proxy is None:
|
|
42
|
+
del os.environ['no_proxy']
|
|
43
|
+
else:
|
|
44
|
+
if env_name_is_lowercase:
|
|
45
|
+
os.environ['no_proxy'] = original_no_proxy
|
|
46
|
+
else:
|
|
47
|
+
os.environ['NO_PROXY'] = original_no_proxy
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_client(*args, **kwargs) -> BaseClient:
|
|
51
|
+
# AWS SDK requires a special setting of 'no_proxy'
|
|
52
|
+
add_aws_link_local_address_to_no_proxy()
|
|
53
|
+
|
|
54
|
+
if 'config' in kwargs:
|
|
55
|
+
raise ValueError('get_client(...) does not allow to specify the "config" parameter.')
|
|
56
|
+
|
|
57
|
+
config = Config(
|
|
58
|
+
retries={
|
|
59
|
+
'max_attempts': 10,
|
|
60
|
+
'mode': 'standard'
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
client = boto3.client(*args, **kwargs, config=config)
|
|
64
|
+
return client
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
|
|
10
|
+
from botocore.exceptions import ClientError
|
|
11
|
+
from pycognito.aws_srp import AWSSRP
|
|
12
|
+
from pycognito.exceptions import ForceChangePasswordException, SoftwareTokenMFAChallengeException
|
|
13
|
+
|
|
14
|
+
from fujitsu_quantum import aws_utils
|
|
15
|
+
from fujitsu_quantum.auth import FjqAuthBoto3, FjqAuthError
|
|
16
|
+
from fujitsu_quantum.config import Config
|
|
17
|
+
|
|
18
|
+
# Used to make debugging faster. Input your testing user credentials,
|
|
19
|
+
# and when the CLI asks for a username and password, just click Enter.
|
|
20
|
+
Username = ""
|
|
21
|
+
Password = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
try:
|
|
26
|
+
parser = argparse.ArgumentParser(description="Fujitsu Quantum CLI")
|
|
27
|
+
parser.add_argument("command", choices=["login", "refresh", "logout", "auth-status"],
|
|
28
|
+
help="Command to run")
|
|
29
|
+
parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode")
|
|
30
|
+
options = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
if options.debug:
|
|
33
|
+
logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)
|
|
34
|
+
logging.debug("Set log level to DEBUG")
|
|
35
|
+
else:
|
|
36
|
+
logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)
|
|
37
|
+
|
|
38
|
+
if options.command == "login":
|
|
39
|
+
user = input("Enter username: ") or Username
|
|
40
|
+
while len(user) == 0:
|
|
41
|
+
user = input("Enter username: ") or Username
|
|
42
|
+
password = getpass.getpass("Enter password: ") or Password
|
|
43
|
+
|
|
44
|
+
client = aws_utils.get_client("cognito-idp", region_name="ap-northeast-1")
|
|
45
|
+
aws = AWSSRP(
|
|
46
|
+
client=client,
|
|
47
|
+
pool_id=Config.auth_pool_id,
|
|
48
|
+
client_id=Config.auth_sdk_client_id,
|
|
49
|
+
username=user,
|
|
50
|
+
password=password,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
logging.debug("AWSSRP.authenticate")
|
|
55
|
+
aws.authenticate_user()
|
|
56
|
+
|
|
57
|
+
except ForceChangePasswordException:
|
|
58
|
+
print("Your account is in an invalid state. Please contact the support team.", file=sys.stderr)
|
|
59
|
+
raise RuntimeError("Invalid account state.")
|
|
60
|
+
|
|
61
|
+
except SoftwareTokenMFAChallengeException as exc:
|
|
62
|
+
logging.debug("pycognito.SoftwareTokenMFAChallengeException")
|
|
63
|
+
|
|
64
|
+
device_name = exc.get_tokens()["ChallengeParameters"].get("FRIENDLY_DEVICE_NAME", None)
|
|
65
|
+
if device_name is not None:
|
|
66
|
+
code_prompt = "Enter the 6-digit code generated by your TOTP device ({0}): ".format(device_name)
|
|
67
|
+
else:
|
|
68
|
+
code_prompt = "Enter the 6-digit code generated by your TOTP device): "
|
|
69
|
+
|
|
70
|
+
code = input(code_prompt)
|
|
71
|
+
while len(code) == 0:
|
|
72
|
+
code = input(code_prompt)
|
|
73
|
+
|
|
74
|
+
current_time = datetime.now()
|
|
75
|
+
tokens = client.respond_to_auth_challenge(
|
|
76
|
+
ClientId=Config.auth_sdk_client_id,
|
|
77
|
+
Session=exc.get_tokens()["Session"],
|
|
78
|
+
ChallengeName=exc.get_tokens()["ChallengeName"],
|
|
79
|
+
ChallengeResponses={
|
|
80
|
+
"USERNAME": user,
|
|
81
|
+
"SOFTWARE_TOKEN_MFA_CODE": code,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
access_token = tokens["AuthenticationResult"]["AccessToken"]
|
|
86
|
+
refresh_token = tokens["AuthenticationResult"]["RefreshToken"]
|
|
87
|
+
access_token_expires_in = tokens["AuthenticationResult"]["ExpiresIn"]
|
|
88
|
+
access_token_expiration_time = current_time + timedelta(seconds=access_token_expires_in)
|
|
89
|
+
refresh_token_expiration_time = current_time + timedelta(days=180)
|
|
90
|
+
|
|
91
|
+
if "NewDeviceMetadata" in tokens["AuthenticationResult"]:
|
|
92
|
+
logging.debug("Confirming device...")
|
|
93
|
+
_, _ = aws.confirm_device(tokens=tokens)
|
|
94
|
+
device_key = tokens["AuthenticationResult"]["NewDeviceMetadata"]["DeviceKey"]
|
|
95
|
+
else:
|
|
96
|
+
device_key = None
|
|
97
|
+
|
|
98
|
+
print("Authentication succeeded.")
|
|
99
|
+
|
|
100
|
+
FjqAuthBoto3.store_credentials(
|
|
101
|
+
client_id=Config.auth_sdk_client_id,
|
|
102
|
+
username=user,
|
|
103
|
+
access_token=access_token,
|
|
104
|
+
access_token_expiration_time=access_token_expiration_time,
|
|
105
|
+
refresh_token=refresh_token,
|
|
106
|
+
refresh_token_expiration_time=refresh_token_expiration_time,
|
|
107
|
+
device_key=device_key,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
print("Successfully logged in.")
|
|
111
|
+
|
|
112
|
+
elif options.command == "refresh":
|
|
113
|
+
FjqAuthBoto3().refresh_auth(force=True)
|
|
114
|
+
print("The credential file has been successfully updated.")
|
|
115
|
+
|
|
116
|
+
elif options.command == "logout":
|
|
117
|
+
FjqAuthBoto3().logout()
|
|
118
|
+
print("Successfully logged out.")
|
|
119
|
+
|
|
120
|
+
elif options.command == 'auth-status':
|
|
121
|
+
try:
|
|
122
|
+
auth = FjqAuthBoto3()
|
|
123
|
+
expr_time = datetime.strptime(auth.refresh_token_expiration_time, '%Y-%m-%d %H:%M:%S')
|
|
124
|
+
if expr_time < datetime.now():
|
|
125
|
+
print('Credentials are outdated.\nPlease run `fujitsu-quantum login` to update the credentials.')
|
|
126
|
+
else:
|
|
127
|
+
print(f'You have valid credentials.\nThe credentials will expire at {expr_time}.')
|
|
128
|
+
except FjqAuthError as e:
|
|
129
|
+
print(e.args[0])
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
print(f"Invalid command: {options.command}")
|
|
133
|
+
|
|
134
|
+
except ClientError as exc:
|
|
135
|
+
boto3_error_code = exc.response["Error"]["Code"]
|
|
136
|
+
boto3_error_message = exc.response["Error"]["Message"]
|
|
137
|
+
if boto3_error_code == 'NotAuthorizedException':
|
|
138
|
+
error_message = f'Failed to {options.command}.\n{boto3_error_message}.'
|
|
139
|
+
elif boto3_error_code == 'CodeMismatchException':
|
|
140
|
+
error_message = 'Failed to login.\nThe TOTP code is invalid.'
|
|
141
|
+
else:
|
|
142
|
+
error_message = f'Failed to login.\n{boto3_error_message}. ({boto3_error_code})'
|
|
143
|
+
|
|
144
|
+
print(error_message)
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
print(''.join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
5
|
+
from typing import List, Union
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 9):
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
else:
|
|
10
|
+
# collections.abc.Sequence is not subscriptable in Python 3.8 or older.
|
|
11
|
+
# collections.abc.Sequence is deprecated since Python 3.9
|
|
12
|
+
from typing import Sequence
|
|
13
|
+
|
|
14
|
+
from fujitsu_quantum.devices import QPU, Simulator
|
|
15
|
+
from fujitsu_quantum.results import EstimationResult, SamplingResult
|
|
16
|
+
from fujitsu_quantum.tasks import EstimationTask, SamplingTask
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Executor:
|
|
20
|
+
"""This class enables users to simultaneously submit multiple quantum tasks and retrieve results."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, max_workers: int):
|
|
23
|
+
self.executor = ProcessPoolExecutor(max_workers=max_workers)
|
|
24
|
+
|
|
25
|
+
def submit_sampling_tasks(self, device: Union[QPU, Simulator], code_list, *args, **kwargs) -> List[SamplingTask]:
|
|
26
|
+
task_futures = []
|
|
27
|
+
for code in code_list:
|
|
28
|
+
if kwargs is None:
|
|
29
|
+
kwargs = {'code': code}
|
|
30
|
+
else:
|
|
31
|
+
kwargs['code'] = code
|
|
32
|
+
|
|
33
|
+
task_futures.append(self.executor.submit(device.submit_sampling_task, *args, **kwargs))
|
|
34
|
+
|
|
35
|
+
tasks = [tf.result() for tf in task_futures]
|
|
36
|
+
return tasks
|
|
37
|
+
|
|
38
|
+
def submit_estimation_tasks(self, device: Union[QPU, Simulator], code_list, *args, **kwargs)\
|
|
39
|
+
-> List[EstimationTask]:
|
|
40
|
+
|
|
41
|
+
task_futures = []
|
|
42
|
+
for code in code_list:
|
|
43
|
+
if kwargs is None:
|
|
44
|
+
kwargs = {'code': code}
|
|
45
|
+
else:
|
|
46
|
+
kwargs['code'] = code
|
|
47
|
+
|
|
48
|
+
task_futures.append(self.executor.submit(device.submit_estimation_task, *args, **kwargs))
|
|
49
|
+
|
|
50
|
+
tasks = [tf.result() for tf in task_futures]
|
|
51
|
+
return tasks
|
|
52
|
+
|
|
53
|
+
def get_results(self, tasks: Sequence[Union[EstimationTask, SamplingTask]])\
|
|
54
|
+
-> List[Union[EstimationResult, SamplingResult]]:
|
|
55
|
+
|
|
56
|
+
result_futures = []
|
|
57
|
+
for task in tasks:
|
|
58
|
+
result_futures.append(self.executor.submit(Executor._get_task_result, task))
|
|
59
|
+
|
|
60
|
+
results = [rf.result() for rf in result_futures]
|
|
61
|
+
return results
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _get_task_result(task: Union[EstimationTask, SamplingTask]) -> Union[EstimationResult, SamplingResult]:
|
|
65
|
+
return task.result()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# (C) 2024 Fujitsu Limited
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pyjson5
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(frozen=True)
|
|
10
|
+
class _Config:
|
|
11
|
+
config_dir: str
|
|
12
|
+
auth_pool_id: str
|
|
13
|
+
auth_sdk_client_id: str
|
|
14
|
+
api_base: str
|
|
15
|
+
result_polling_interval: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config() -> _Config:
|
|
19
|
+
config_dir = Path.home() / '.fujitsu-quantum'
|
|
20
|
+
config_file_path = config_dir / 'config.json'
|
|
21
|
+
|
|
22
|
+
if config_file_path.exists():
|
|
23
|
+
with open(config_file_path, "r", encoding='utf-8') as f:
|
|
24
|
+
conf_data = pyjson5.decode_io(f)
|
|
25
|
+
|
|
26
|
+
if not isinstance(conf_data, dict):
|
|
27
|
+
raise ValueError(f"Invalid config file: {config_file_path}")
|
|
28
|
+
|
|
29
|
+
conf = conf_data
|
|
30
|
+
else:
|
|
31
|
+
conf = {}
|
|
32
|
+
|
|
33
|
+
available_keys = {'authPoolId', 'authSdkClientId', 'apiBase', 'resultPollingInterval'}
|
|
34
|
+
invalid_keys = conf.keys() - available_keys
|
|
35
|
+
if invalid_keys:
|
|
36
|
+
raise ValueError(f'Invalid parameter names in config.json: {invalid_keys}\n'
|
|
37
|
+
f'config.json path: {config_file_path}')
|
|
38
|
+
|
|
39
|
+
# validate the config values
|
|
40
|
+
result_polling_interval = conf.get('resultPollingInterval', 5)
|
|
41
|
+
if result_polling_interval < 1:
|
|
42
|
+
raise ValueError('Invalid value in config.json: resultPollingInterval must be >= 1.\n'
|
|
43
|
+
f'config.json path: {config_file_path}')
|
|
44
|
+
|
|
45
|
+
# Note: some config values can be overridden via the config file for development purpose
|
|
46
|
+
return _Config(
|
|
47
|
+
config_dir=str(config_dir),
|
|
48
|
+
auth_pool_id=conf.get('authPoolId', 'ap-northeast-1_oUVGkT61e'),
|
|
49
|
+
auth_sdk_client_id=conf.get('authSdkClientId', '66l8iadggtteljctngjepi8du0'),
|
|
50
|
+
api_base=conf.get('apiBase', 'https://api.quantum.global.fujitsu.com'),
|
|
51
|
+
result_polling_interval=result_polling_interval,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
Config = load_config()
|