pcss-qapi 0.1.0__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.
- pcss_qapi/__init__.py +5 -0
- pcss_qapi/auth/__init__.py +4 -0
- pcss_qapi/auth/auth_service.py +132 -0
- pcss_qapi/auth/connections.py +15 -0
- pcss_qapi/auth/oauth_flow.py +203 -0
- pcss_qapi/base/__init__.py +0 -0
- pcss_qapi/base/connection_base.py +20 -0
- pcss_qapi/base/job_base.py +52 -0
- pcss_qapi/base/provider_base.py +38 -0
- pcss_qapi/orca/__init__.py +15 -0
- pcss_qapi/orca/backend.py +198 -0
- pcss_qapi/orca/orca_task.py +150 -0
- pcss_qapi/orca/provider.py +65 -0
- pcss_qapi/orca/ptseries_integration/__init__.py +6 -0
- pcss_qapi/orca/ptseries_integration/bbs.py +59 -0
- pcss_qapi/orca/ptseries_integration/orca_layer.py +77 -0
- pcss_qapi/orca/ptseries_integration/pt_adapter.py +113 -0
- pcss_qapi/utils/__init__.py +4 -0
- pcss_qapi/utils/exceptions.py +8 -0
- pcss_qapi/utils/fs_manager.py +66 -0
- pcss_qapi/utils/requests.py +31 -0
- pcss_qapi-0.1.0.dist-info/METADATA +21 -0
- pcss_qapi-0.1.0.dist-info/RECORD +25 -0
- pcss_qapi-0.1.0.dist-info/WHEEL +5 -0
- pcss_qapi-0.1.0.dist-info/top_level.txt +1 -0
pcss_qapi/__init__.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Authorization service for pcss_qapi"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
|
|
7
|
+
from pcss_qapi.auth.oauth_flow import OAuthManager
|
|
8
|
+
from pcss_qapi.utils.fs_manager import FSManager, API_KEY_FILE_NAME
|
|
9
|
+
|
|
10
|
+
from pcss_qapi.base.connection_base import ApiConnection
|
|
11
|
+
from pcss_qapi.auth.connections import PcssQapiConnection
|
|
12
|
+
|
|
13
|
+
OAUTH_MANAGERS: dict[str, OAuthManager] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_manager_for_connection(connection: ApiConnection) -> OAuthManager:
|
|
17
|
+
"""Get oauth manager for given connection type, create new if not existing"""
|
|
18
|
+
connection_manager = OAUTH_MANAGERS.get(str(hash(connection)), None)
|
|
19
|
+
if connection_manager is None:
|
|
20
|
+
connection_manager = OAuthManager(
|
|
21
|
+
connection.oauth_client_id,
|
|
22
|
+
connection.oauth_issuer,
|
|
23
|
+
connection.oauth_min_ttl_seconds
|
|
24
|
+
)
|
|
25
|
+
OAUTH_MANAGERS[str(hash(connection))] = connection_manager
|
|
26
|
+
return connection_manager
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_hash(issuer: str, client_id: str) -> str:
|
|
30
|
+
"""get hash for connection"""
|
|
31
|
+
return hashlib.sha256(f'{issuer}{client_id}'.encode('utf-8')).hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthorizationService:
|
|
35
|
+
"""
|
|
36
|
+
Authentication and login handler for pcss_qapi service
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _get_functional_tokens(oauth_manager: OAuthManager) -> tuple[str | None, str | None]:
|
|
41
|
+
"""Get access and refresh tokens, refresh if necessary."""
|
|
42
|
+
storage_path = FSManager.get_storage_path()
|
|
43
|
+
filepath = os.path.join(storage_path, API_KEY_FILE_NAME)
|
|
44
|
+
|
|
45
|
+
if not os.path.exists(filepath):
|
|
46
|
+
return None, None
|
|
47
|
+
|
|
48
|
+
key_hash = get_hash(oauth_manager.issuer, oauth_manager.client_id)
|
|
49
|
+
|
|
50
|
+
access_token, refresh_token = None, None
|
|
51
|
+
with open(filepath, 'r', encoding='UTF-8') as f:
|
|
52
|
+
key_infos = json.loads(f.read())
|
|
53
|
+
key_info = key_infos.get(key_hash, None)
|
|
54
|
+
if key_info is None:
|
|
55
|
+
return None, None
|
|
56
|
+
|
|
57
|
+
access_token, refresh_token = key_info.get('access_token', None), key_info.get('refresh_token', None)
|
|
58
|
+
if access_token is None or refresh_token is None:
|
|
59
|
+
return None, None
|
|
60
|
+
|
|
61
|
+
if not oauth_manager.is_token_valid(access_token):
|
|
62
|
+
access_token, refresh_token = oauth_manager.get_refreshed_tokens(refresh_token)
|
|
63
|
+
|
|
64
|
+
if access_token is not None and refresh_token is not None:
|
|
65
|
+
FSManager.update_credentials(
|
|
66
|
+
key_hash,
|
|
67
|
+
{
|
|
68
|
+
'refresh_token': refresh_token,
|
|
69
|
+
'access_token': access_token
|
|
70
|
+
}) # Save refreshed tokens
|
|
71
|
+
|
|
72
|
+
return access_token, refresh_token
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def login(
|
|
76
|
+
connection: ApiConnection = PcssQapiConnection,
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
Cache api key string for later use.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
"""
|
|
83
|
+
storage_location = FSManager.get_storage_path()
|
|
84
|
+
|
|
85
|
+
storage_path = FSManager.get_fixed_path(storage_location)
|
|
86
|
+
os.makedirs(storage_path, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
connection_manager = get_manager_for_connection(connection)
|
|
89
|
+
key_hash = get_hash(connection_manager.issuer, connection_manager.client_id)
|
|
90
|
+
|
|
91
|
+
access_token, refresh_token = AuthorizationService._get_functional_tokens(connection_manager)
|
|
92
|
+
if access_token is not None and refresh_token is not None:
|
|
93
|
+
print("ℹ️ Using cached tokens. If you want to change the account use AuthorizationService.logout() first.")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
access_token, refresh_token = connection_manager.get_device_flow_tokens()
|
|
97
|
+
if access_token is None or refresh_token is None:
|
|
98
|
+
raise ValueError("Authorization error, did not receive tokens.")
|
|
99
|
+
|
|
100
|
+
FSManager.update_credentials(
|
|
101
|
+
key_hash,
|
|
102
|
+
{
|
|
103
|
+
'refresh_token': refresh_token,
|
|
104
|
+
'access_token': access_token
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def logout(
|
|
109
|
+
connection: ApiConnection | None = None
|
|
110
|
+
):
|
|
111
|
+
"""Remove credentials for active account"""
|
|
112
|
+
|
|
113
|
+
filepath = os.path.join(FSManager.get_storage_path(), API_KEY_FILE_NAME)
|
|
114
|
+
if connection is None:
|
|
115
|
+
if os.path.exists(filepath):
|
|
116
|
+
os.remove(filepath)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
FSManager.remove_credentials(
|
|
120
|
+
get_hash(connection.oauth_issuer, connection.oauth_client_id)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def get_api_key(connection: ApiConnection) -> str:
|
|
125
|
+
"""Get api key string if available. Automatically logs out if unable to refresh key."""
|
|
126
|
+
|
|
127
|
+
connection_manager = get_manager_for_connection(connection)
|
|
128
|
+
access_token, _ = AuthorizationService._get_functional_tokens(connection_manager)
|
|
129
|
+
if access_token is None:
|
|
130
|
+
AuthorizationService.logout()
|
|
131
|
+
raise ValueError("Use AuthorizationService.login()")
|
|
132
|
+
return access_token
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Connection definitions"""
|
|
2
|
+
|
|
3
|
+
from pcss_qapi.base.connection_base import ApiConnection
|
|
4
|
+
|
|
5
|
+
PcssQapiConnection = ApiConnection(
|
|
6
|
+
oauth_client_id='jupyter.quantum.psnc.pl-notebooks',
|
|
7
|
+
oauth_issuer='https://sso.classroom.pionier.net.pl/auth/realms/Classroom',
|
|
8
|
+
oauth_min_ttl_seconds=60,
|
|
9
|
+
api_health_url='https://api.quantum.psnc.pl/api/health/test-connection',
|
|
10
|
+
task_endpoints_url='https://api.quantum.psnc.pl/api/client/tasks',
|
|
11
|
+
machine_endpoints_url='https://api.quantum.psnc.pl/api/client/machines',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ['PcssQapiConnection']
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""OAuth authorization management"""
|
|
2
|
+
import time
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _print_nothing(width):
|
|
10
|
+
print("".join(' ' for _ in range(width)), end='\r')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _print_formatted_time(time_seconds):
|
|
14
|
+
spinner = "⣾⣽⣻⢿⡿⣟⣯⣷"
|
|
15
|
+
|
|
16
|
+
minutes_left = time_seconds // 60
|
|
17
|
+
seconds_left = time_seconds % 60
|
|
18
|
+
|
|
19
|
+
minutes_string = f'{minutes_left} minutes'
|
|
20
|
+
seconds_string = f'{seconds_left} seconds'
|
|
21
|
+
|
|
22
|
+
s = f"""{spinner[time_seconds % len(spinner)]} Waiting... {" ".join([minutes_string if minutes_left else "",
|
|
23
|
+
seconds_string])} remaining to complete authorization."""
|
|
24
|
+
print(s, end='\r')
|
|
25
|
+
return len(s)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _show_error(error_type: str | None, last_printed_line_len: int):
|
|
29
|
+
if error_type == "authorization_pending":
|
|
30
|
+
pass
|
|
31
|
+
else:
|
|
32
|
+
error_message = {
|
|
33
|
+
'slow_down': '⏳ Server requested slower polling. Increasing interval.',
|
|
34
|
+
'access_denied': '❌ Access was denied by user.',
|
|
35
|
+
None: '❌ Failed to parse polling error response.'
|
|
36
|
+
}.get(error_type, '❌ Failed to parse polling error response.')
|
|
37
|
+
|
|
38
|
+
_print_nothing(last_printed_line_len)
|
|
39
|
+
print(error_message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _decode_jwt_payload(token):
|
|
43
|
+
try:
|
|
44
|
+
parts = token.split(".")
|
|
45
|
+
if len(parts) != 3:
|
|
46
|
+
return None
|
|
47
|
+
padded = parts[1] + "=" * (-len(parts[1]) % 4)
|
|
48
|
+
decoded_bytes = base64.urlsafe_b64decode(padded)
|
|
49
|
+
return json.loads(decoded_bytes)
|
|
50
|
+
except Exception: # pylint:disable = broad-exception-caught
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OAuthManager:
|
|
55
|
+
"""Token handler"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, client_id, issuer, min_ttl) -> None:
|
|
58
|
+
self.client_id = client_id
|
|
59
|
+
self.issuer = issuer
|
|
60
|
+
self.min_ttl = min_ttl
|
|
61
|
+
|
|
62
|
+
self.token_endpoint: str | None = None
|
|
63
|
+
self.device_endpoint: str | None = None
|
|
64
|
+
|
|
65
|
+
self._try_get_urls()
|
|
66
|
+
|
|
67
|
+
def _try_get_urls(self) -> bool:
|
|
68
|
+
success = False
|
|
69
|
+
try:
|
|
70
|
+
discovery_url = f"{self.issuer}/.well-known/openid-configuration"
|
|
71
|
+
discovery = requests.get(discovery_url, timeout=5).json()
|
|
72
|
+
self.token_endpoint = discovery["token_endpoint"]
|
|
73
|
+
self.device_endpoint = discovery["device_authorization_endpoint"]
|
|
74
|
+
success = True
|
|
75
|
+
except requests.exceptions.ConnectionError:
|
|
76
|
+
pass
|
|
77
|
+
return success
|
|
78
|
+
|
|
79
|
+
def is_token_valid(self, token):
|
|
80
|
+
"""Check if token will be valid for more than self.min_ttl seconds."""
|
|
81
|
+
payload = _decode_jwt_payload(token)
|
|
82
|
+
if not payload or "exp" not in payload:
|
|
83
|
+
return False
|
|
84
|
+
exp = payload["exp"]
|
|
85
|
+
return (exp - int(time.time())) >= self.min_ttl
|
|
86
|
+
|
|
87
|
+
def get_refreshed_tokens(self, refresh_token) -> tuple[str | None, str | None]:
|
|
88
|
+
"""Get new access and refresh tokens fr"""
|
|
89
|
+
if (self.token_endpoint is None or self.device_endpoint is None) and not self._try_get_urls():
|
|
90
|
+
raise RuntimeError("Authentication server inaccessible")
|
|
91
|
+
|
|
92
|
+
resp = requests.post(
|
|
93
|
+
self.token_endpoint,
|
|
94
|
+
data={
|
|
95
|
+
"grant_type": "refresh_token",
|
|
96
|
+
"client_id": self.client_id,
|
|
97
|
+
"refresh_token": refresh_token
|
|
98
|
+
},
|
|
99
|
+
timeout=20)
|
|
100
|
+
if resp.ok:
|
|
101
|
+
data = resp.json()
|
|
102
|
+
return data.get("access_token"), data.get("refresh_token")
|
|
103
|
+
|
|
104
|
+
# print(f"⚠️ Refresh grant failed (HTTP {resp.status_code}): {resp.text}")
|
|
105
|
+
return None, None
|
|
106
|
+
|
|
107
|
+
def _get_device_flow_data(self) -> dict[str, Any]:
|
|
108
|
+
response = requests.post(
|
|
109
|
+
self.device_endpoint,
|
|
110
|
+
data={
|
|
111
|
+
"client_id": self.client_id
|
|
112
|
+
},
|
|
113
|
+
timeout=20)
|
|
114
|
+
if not response.ok:
|
|
115
|
+
try:
|
|
116
|
+
error_data = response.json()
|
|
117
|
+
error = error_data.get("error", "unknown_error")
|
|
118
|
+
description = error_data.get("error_description", "No description.")
|
|
119
|
+
except Exception: # pylint:disable = broad-exception-caught
|
|
120
|
+
error = "unknown_error"
|
|
121
|
+
description = response.text
|
|
122
|
+
raise RuntimeError(f"Device Authorization Error: {error} — {description}") # pylint:disable = broad-exception-raised
|
|
123
|
+
|
|
124
|
+
if 'application/json' not in response.headers.get('content-type', ''):
|
|
125
|
+
raise ValueError("Invalid device flow response.")
|
|
126
|
+
|
|
127
|
+
data = response.json()
|
|
128
|
+
required_fields = ["verification_uri_complete", "device_code", "expires_in"]
|
|
129
|
+
if not all(field in data for field in required_fields):
|
|
130
|
+
raise ValueError("Missing expected fields in device flow response.") # pylint:disable = broad-exception-raised
|
|
131
|
+
|
|
132
|
+
return data
|
|
133
|
+
|
|
134
|
+
def _poll_auth_server(self, device_code) -> tuple[str | None, str | None, str | None]:
|
|
135
|
+
poll = requests.post(
|
|
136
|
+
self.token_endpoint,
|
|
137
|
+
data={
|
|
138
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
139
|
+
"device_code": device_code,
|
|
140
|
+
"client_id": self.client_id
|
|
141
|
+
},
|
|
142
|
+
timeout=20)
|
|
143
|
+
if poll.status_code == 200:
|
|
144
|
+
t = poll.json()
|
|
145
|
+
return t.get("access_token"), t.get("refresh_token"), None
|
|
146
|
+
if poll.status_code == 400:
|
|
147
|
+
err = None
|
|
148
|
+
try:
|
|
149
|
+
err = poll.json().get("error")
|
|
150
|
+
except requests.JSONDecodeError:
|
|
151
|
+
pass
|
|
152
|
+
return None, None, err
|
|
153
|
+
return None, None, None
|
|
154
|
+
|
|
155
|
+
def get_device_flow_tokens(self) -> tuple[str | None, str | None]: # pylint:disable = too-many-statements,too-many-locals
|
|
156
|
+
"""Get new access and refresh tokens from device flow."""
|
|
157
|
+
if (self.token_endpoint is None or self.device_endpoint is None) and not self._try_get_urls():
|
|
158
|
+
raise RuntimeError("Authentication server inaccessible")
|
|
159
|
+
|
|
160
|
+
data = self._get_device_flow_data()
|
|
161
|
+
|
|
162
|
+
print("\n🔐 Authorize Access")
|
|
163
|
+
print("----------------------------------------")
|
|
164
|
+
print("You are about to be redirected to an authorization server.")
|
|
165
|
+
print("There, you will be asked to grant access permissions.")
|
|
166
|
+
print("This allows the system to act on your behalf using delegated access.")
|
|
167
|
+
print("Please confirm only if you trust this application.")
|
|
168
|
+
print(f"➡️ Click to authorize: {data['verification_uri_complete']}")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
device_code = data["device_code"]
|
|
172
|
+
interval = data.get("interval", 5)
|
|
173
|
+
expires_in = data["expires_in"]
|
|
174
|
+
|
|
175
|
+
sleep_time = 1
|
|
176
|
+
interval_count = 0
|
|
177
|
+
|
|
178
|
+
last_printed_line_len = 0
|
|
179
|
+
while expires_in > 0:
|
|
180
|
+
if interval_count < 0:
|
|
181
|
+
interval_count = interval
|
|
182
|
+
|
|
183
|
+
access_token, refresh_token, error_type = self._poll_auth_server(device_code)
|
|
184
|
+
|
|
185
|
+
if access_token is not None and refresh_token is not None:
|
|
186
|
+
_print_nothing(last_printed_line_len)
|
|
187
|
+
print("✅ Access granted.")
|
|
188
|
+
return access_token, refresh_token
|
|
189
|
+
|
|
190
|
+
_show_error(error_type, last_printed_line_len)
|
|
191
|
+
|
|
192
|
+
if error_type == 'slow_down':
|
|
193
|
+
interval += 1
|
|
194
|
+
elif error_type != 'authorization_pending':
|
|
195
|
+
return None, None
|
|
196
|
+
|
|
197
|
+
expires_in -= sleep_time
|
|
198
|
+
interval_count -= sleep_time
|
|
199
|
+
last_printed_line_len = _print_formatted_time(expires_in)
|
|
200
|
+
time.sleep(sleep_time)
|
|
201
|
+
|
|
202
|
+
print("\n❌ Timeout: Authorization not completed in time.")
|
|
203
|
+
return None, None
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Connection base"""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class ApiConnection:
|
|
7
|
+
"""Base class for defining a new api connection"""
|
|
8
|
+
|
|
9
|
+
oauth_client_id: str
|
|
10
|
+
oauth_issuer: str
|
|
11
|
+
oauth_min_ttl_seconds: int
|
|
12
|
+
api_health_url: str
|
|
13
|
+
task_endpoints_url: str
|
|
14
|
+
machine_endpoints_url: str
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return f"<{self.task_endpoints_url.replace('https://', '').replace('http://', '').split('/', maxsplit=1)[0]}>"
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return str(self)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Job ABCs"""
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from pcss_qapi.utils import FSManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseRemoteJob(ABC):
|
|
10
|
+
"""Blueprint for an api remote job (non qiskit)"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, job_type: str) -> None:
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.uid = None
|
|
15
|
+
self.type = job_type # Provider type
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def _is_remote_available(self) -> bool:
|
|
19
|
+
"""Check if remote api is up"""
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def _submit(self):
|
|
22
|
+
"""Submit job to remote"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def _get_job_metadata(self) -> dict:
|
|
26
|
+
"""Additional metadata to save"""
|
|
27
|
+
|
|
28
|
+
def _save_job(self):
|
|
29
|
+
save_path = FSManager.get_task_directory(self.type)
|
|
30
|
+
os.makedirs(save_path, exist_ok=True)
|
|
31
|
+
with open(os.path.join(save_path, f'{self.uid}.json'), 'w+', encoding='UTF-8') as f:
|
|
32
|
+
f.write(json.dumps(self._get_job_metadata()))
|
|
33
|
+
|
|
34
|
+
def submit(self) -> None:
|
|
35
|
+
"""Submit job to remote"""
|
|
36
|
+
if not self._is_remote_available():
|
|
37
|
+
raise ValueError('Remote server is unreachable')
|
|
38
|
+
self._submit()
|
|
39
|
+
self._save_job()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def status(self) -> Any:
|
|
44
|
+
"""Task status"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def results(self) -> Any:
|
|
48
|
+
"""Get task results"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def cancel(self) -> bool:
|
|
52
|
+
"""Attempt to cancel task if it is queued, return True if successful"""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Provider ABCs"""
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pcss_qapi.base.connection_base import ApiConnection
|
|
6
|
+
from pcss_qapi.auth.connections import PcssQapiConnection
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseProvider(ABC):
|
|
10
|
+
"""Backend provider ABC"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
connection: ApiConnection = PcssQapiConnection
|
|
15
|
+
) -> None:
|
|
16
|
+
self.connection = connection
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_backend(self, backend_name) -> Any:
|
|
20
|
+
"""Get backend with the name of backend_name"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def available_backends(self, simulators: bool = False) -> list[str]:
|
|
24
|
+
"""
|
|
25
|
+
Return names of available backends for the authed user.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
list[str]: Backend names.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def least_busy(self) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
Return least busy backend from the pool of **real** backends.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
object: Least busy backend.
|
|
38
|
+
"""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""ORCA Computing providers, backends, etc."""
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
from pcss_qapi.utils.exceptions import MissingOptionalDependencyError
|
|
5
|
+
from .orca_task import OrcaTask
|
|
6
|
+
|
|
7
|
+
__all__ = ['OrcaTask']
|
|
8
|
+
|
|
9
|
+
# We want OrcaTask to be available even without ptseries
|
|
10
|
+
try:
|
|
11
|
+
from .backend import OrcaBackend
|
|
12
|
+
from .provider import OrcaProvider
|
|
13
|
+
__all__ += ['OrcaProvider', 'OrcaBackend']
|
|
14
|
+
except MissingOptionalDependencyError as e:
|
|
15
|
+
warnings.warn(e.msg)
|