qi-compute-api-client 0.56.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.
- compute_api_client/__init__.py +219 -0
- compute_api_client/api/__init__.py +23 -0
- compute_api_client/api/algorithms_api.py +1603 -0
- compute_api_client/api/auth_config_api.py +278 -0
- compute_api_client/api/backend_api.py +1540 -0
- compute_api_client/api/backend_types_api.py +1178 -0
- compute_api_client/api/batch_jobs_api.py +2224 -0
- compute_api_client/api/commits_api.py +1565 -0
- compute_api_client/api/files_api.py +1307 -0
- compute_api_client/api/final_results_api.py +847 -0
- compute_api_client/api/health_api.py +281 -0
- compute_api_client/api/jobs_api.py +1787 -0
- compute_api_client/api/languages_api.py +692 -0
- compute_api_client/api/members_api.py +1238 -0
- compute_api_client/api/metadata_api.py +930 -0
- compute_api_client/api/permissions_api.py +1295 -0
- compute_api_client/api/projects_api.py +1889 -0
- compute_api_client/api/reservations_api.py +1324 -0
- compute_api_client/api/results_api.py +1702 -0
- compute_api_client/api/teams_api.py +692 -0
- compute_api_client/api/transactions_api.py +805 -0
- compute_api_client/api/users_api.py +1305 -0
- compute_api_client/api_client.py +804 -0
- compute_api_client/api_response.py +21 -0
- compute_api_client/configuration.py +606 -0
- compute_api_client/docs/Algorithm.md +34 -0
- compute_api_client/docs/AlgorithmIn.md +33 -0
- compute_api_client/docs/AlgorithmType.md +12 -0
- compute_api_client/docs/AlgorithmsApi.md +428 -0
- compute_api_client/docs/AuthConfig.md +31 -0
- compute_api_client/docs/AuthConfigApi.md +71 -0
- compute_api_client/docs/Backend.md +35 -0
- compute_api_client/docs/BackendApi.md +418 -0
- compute_api_client/docs/BackendIn.md +34 -0
- compute_api_client/docs/BackendMessage.md +29 -0
- compute_api_client/docs/BackendPatch.md +31 -0
- compute_api_client/docs/BackendStatus.md +16 -0
- compute_api_client/docs/BackendType.md +48 -0
- compute_api_client/docs/BackendTypePatch.md +45 -0
- compute_api_client/docs/BackendTypesApi.md +289 -0
- compute_api_client/docs/BackendWithAuthentication.md +36 -0
- compute_api_client/docs/BatchJob.md +39 -0
- compute_api_client/docs/BatchJobIn.md +29 -0
- compute_api_client/docs/BatchJobStatus.md +18 -0
- compute_api_client/docs/BatchJobsApi.md +600 -0
- compute_api_client/docs/Commit.md +33 -0
- compute_api_client/docs/CommitIn.md +30 -0
- compute_api_client/docs/CommitsApi.md +425 -0
- compute_api_client/docs/CompilePayload.md +30 -0
- compute_api_client/docs/CompileStage.md +18 -0
- compute_api_client/docs/Domain.md +14 -0
- compute_api_client/docs/File.md +36 -0
- compute_api_client/docs/FileIn.md +35 -0
- compute_api_client/docs/FilesApi.md +346 -0
- compute_api_client/docs/FinalResult.md +32 -0
- compute_api_client/docs/FinalResultIn.md +30 -0
- compute_api_client/docs/FinalResultsApi.md +248 -0
- compute_api_client/docs/HTTPBadRequestError.md +29 -0
- compute_api_client/docs/HTTPNotFoundError.md +29 -0
- compute_api_client/docs/HTTPValidationError.md +29 -0
- compute_api_client/docs/HealthApi.md +72 -0
- compute_api_client/docs/Job.md +42 -0
- compute_api_client/docs/JobIn.md +32 -0
- compute_api_client/docs/JobPatch.md +34 -0
- compute_api_client/docs/JobStatus.md +18 -0
- compute_api_client/docs/JobsApi.md +460 -0
- compute_api_client/docs/Language.md +31 -0
- compute_api_client/docs/LanguagesApi.md +177 -0
- compute_api_client/docs/LocationInner.md +27 -0
- compute_api_client/docs/Member.md +33 -0
- compute_api_client/docs/MemberId.md +28 -0
- compute_api_client/docs/MemberIn.md +32 -0
- compute_api_client/docs/MembersApi.md +331 -0
- compute_api_client/docs/PageAlgorithm.md +33 -0
- compute_api_client/docs/PageBackend.md +33 -0
- compute_api_client/docs/PageBackendType.md +33 -0
- compute_api_client/docs/PageBatchJob.md +33 -0
- compute_api_client/docs/PageCommit.md +33 -0
- compute_api_client/docs/PageFile.md +33 -0
- compute_api_client/docs/PageJob.md +33 -0
- compute_api_client/docs/PageLanguage.md +33 -0
- compute_api_client/docs/PageMember.md +33 -0
- compute_api_client/docs/PageMetadata.md +32 -0
- compute_api_client/docs/PagePermission.md +33 -0
- compute_api_client/docs/PagePermissionGroup.md +33 -0
- compute_api_client/docs/PageProject.md +33 -0
- compute_api_client/docs/PageReservation.md +33 -0
- compute_api_client/docs/PageResult.md +33 -0
- compute_api_client/docs/PageTeam.md +33 -0
- compute_api_client/docs/PageTransaction.md +33 -0
- compute_api_client/docs/PageUser.md +33 -0
- compute_api_client/docs/Permission.md +31 -0
- compute_api_client/docs/PermissionGroup.md +30 -0
- compute_api_client/docs/PermissionsApi.md +340 -0
- compute_api_client/docs/Project.md +34 -0
- compute_api_client/docs/ProjectIn.md +32 -0
- compute_api_client/docs/ProjectPatch.md +32 -0
- compute_api_client/docs/ProjectsApi.md +502 -0
- compute_api_client/docs/Reservation.md +35 -0
- compute_api_client/docs/ReservationIn.md +32 -0
- compute_api_client/docs/ReservationsApi.md +341 -0
- compute_api_client/docs/Result.md +36 -0
- compute_api_client/docs/ResultIn.md +34 -0
- compute_api_client/docs/ResultsApi.md +439 -0
- compute_api_client/docs/Role.md +12 -0
- compute_api_client/docs/ShareType.md +14 -0
- compute_api_client/docs/Team.md +32 -0
- compute_api_client/docs/TeamsApi.md +177 -0
- compute_api_client/docs/Transaction.md +35 -0
- compute_api_client/docs/TransactionDomain.md +28 -0
- compute_api_client/docs/TransactionsApi.md +190 -0
- compute_api_client/docs/User.md +36 -0
- compute_api_client/docs/UserIn.md +35 -0
- compute_api_client/docs/UsersApi.md +338 -0
- compute_api_client/docs/ValidationError.md +31 -0
- compute_api_client/docs/ValidationErrorLocInner.md +28 -0
- compute_api_client/exceptions.py +216 -0
- compute_api_client/models/__init__.py +84 -0
- compute_api_client/models/algorithm.py +105 -0
- compute_api_client/models/algorithm_in.py +103 -0
- compute_api_client/models/algorithm_type.py +37 -0
- compute_api_client/models/auth_config.py +91 -0
- compute_api_client/models/backend.py +106 -0
- compute_api_client/models/backend_in.py +104 -0
- compute_api_client/models/backend_message.py +87 -0
- compute_api_client/models/backend_patch.py +112 -0
- compute_api_client/models/backend_status.py +39 -0
- compute_api_client/models/backend_type.py +145 -0
- compute_api_client/models/backend_type_patch.py +205 -0
- compute_api_client/models/backend_with_authentication.py +108 -0
- compute_api_client/models/batch_job.py +130 -0
- compute_api_client/models/batch_job_in.py +87 -0
- compute_api_client/models/batch_job_status.py +40 -0
- compute_api_client/models/commit.py +97 -0
- compute_api_client/models/commit_in.py +89 -0
- compute_api_client/models/compile_payload.py +95 -0
- compute_api_client/models/compile_stage.py +40 -0
- compute_api_client/models/domain.py +38 -0
- compute_api_client/models/file.py +108 -0
- compute_api_client/models/file_in.py +106 -0
- compute_api_client/models/final_result.py +94 -0
- compute_api_client/models/final_result_in.py +89 -0
- compute_api_client/models/http_bad_request_error.py +87 -0
- compute_api_client/models/http_not_found_error.py +87 -0
- compute_api_client/models/http_validation_error.py +95 -0
- compute_api_client/models/job.py +131 -0
- compute_api_client/models/job_in.py +98 -0
- compute_api_client/models/job_patch.py +99 -0
- compute_api_client/models/job_status.py +40 -0
- compute_api_client/models/language.py +92 -0
- compute_api_client/models/location_inner.py +144 -0
- compute_api_client/models/member.py +96 -0
- compute_api_client/models/member_id.py +144 -0
- compute_api_client/models/member_in.py +94 -0
- compute_api_client/models/metadata.py +93 -0
- compute_api_client/models/metadata_in.py +94 -0
- compute_api_client/models/page_algorithm.py +104 -0
- compute_api_client/models/page_backend.py +104 -0
- compute_api_client/models/page_backend_type.py +104 -0
- compute_api_client/models/page_batch_job.py +104 -0
- compute_api_client/models/page_commit.py +104 -0
- compute_api_client/models/page_file.py +104 -0
- compute_api_client/models/page_job.py +104 -0
- compute_api_client/models/page_language.py +104 -0
- compute_api_client/models/page_member.py +104 -0
- compute_api_client/models/page_metadata.py +125 -0
- compute_api_client/models/page_permission.py +104 -0
- compute_api_client/models/page_permission_group.py +104 -0
- compute_api_client/models/page_project.py +104 -0
- compute_api_client/models/page_reservation.py +104 -0
- compute_api_client/models/page_result.py +104 -0
- compute_api_client/models/page_team.py +104 -0
- compute_api_client/models/page_transaction.py +104 -0
- compute_api_client/models/page_user.py +104 -0
- compute_api_client/models/permission.py +92 -0
- compute_api_client/models/permission_group.py +90 -0
- compute_api_client/models/project.py +99 -0
- compute_api_client/models/project_in.py +94 -0
- compute_api_client/models/project_patch.py +114 -0
- compute_api_client/models/reservation.py +105 -0
- compute_api_client/models/reservation_in.py +94 -0
- compute_api_client/models/result.py +122 -0
- compute_api_client/models/result_in.py +117 -0
- compute_api_client/models/role.py +37 -0
- compute_api_client/models/share_type.py +38 -0
- compute_api_client/models/team.py +94 -0
- compute_api_client/models/transaction.py +117 -0
- compute_api_client/models/transaction_domain.py +142 -0
- compute_api_client/models/user.py +102 -0
- compute_api_client/models/user_in.py +100 -0
- compute_api_client/models/validation_error.py +99 -0
- compute_api_client/models/validation_error_loc_inner.py +138 -0
- compute_api_client/rest.py +213 -0
- qi2_shared/__init__.py +0 -0
- qi2_shared/authentication.py +66 -0
- qi2_shared/client.py +52 -0
- qi2_shared/hybrid/__init__.py +0 -0
- qi2_shared/hybrid/quantum_interface.py +45 -0
- qi2_shared/pagination.py +44 -0
- qi2_shared/settings.py +68 -0
- qi2_shared/utils.py +13 -0
- qi_compute_api_client-0.56.0.dist-info/METADATA +266 -0
- qi_compute_api_client-0.56.0.dist-info/RECORD +205 -0
- qi_compute_api_client-0.56.0.dist-info/WHEEL +4 -0
- qi_compute_api_client-0.56.0.dist-info/licenses/LICENSE.md +142 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Quantum Inspire 2
|
|
5
|
+
|
|
6
|
+
No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
|
7
|
+
|
|
8
|
+
The version of the OpenAPI document: 0.1.0
|
|
9
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
10
|
+
|
|
11
|
+
Do not edit the class manually.
|
|
12
|
+
""" # noqa: E501
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import ssl
|
|
19
|
+
from typing import Optional, Union
|
|
20
|
+
|
|
21
|
+
import aiohttp
|
|
22
|
+
import aiohttp_retry
|
|
23
|
+
|
|
24
|
+
from compute_api_client.exceptions import ApiException, ApiValueError
|
|
25
|
+
|
|
26
|
+
RESTResponseType = aiohttp.ClientResponse
|
|
27
|
+
|
|
28
|
+
ALLOW_RETRY_METHODS = frozenset({'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE'})
|
|
29
|
+
|
|
30
|
+
class RESTResponse(io.IOBase):
|
|
31
|
+
|
|
32
|
+
def __init__(self, resp) -> None:
|
|
33
|
+
self.response = resp
|
|
34
|
+
self.status = resp.status
|
|
35
|
+
self.reason = resp.reason
|
|
36
|
+
self.data = None
|
|
37
|
+
|
|
38
|
+
async def read(self):
|
|
39
|
+
if self.data is None:
|
|
40
|
+
self.data = await self.response.read()
|
|
41
|
+
return self.data
|
|
42
|
+
|
|
43
|
+
def getheaders(self):
|
|
44
|
+
"""Returns a CIMultiDictProxy of the response headers."""
|
|
45
|
+
return self.response.headers
|
|
46
|
+
|
|
47
|
+
def getheader(self, name, default=None):
|
|
48
|
+
"""Returns a given response header."""
|
|
49
|
+
return self.response.headers.get(name, default)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RESTClientObject:
|
|
53
|
+
|
|
54
|
+
def __init__(self, configuration) -> None:
|
|
55
|
+
|
|
56
|
+
# maxsize is number of requests to host that are allowed in parallel
|
|
57
|
+
self.maxsize = configuration.connection_pool_maxsize
|
|
58
|
+
|
|
59
|
+
self.ssl_context = ssl.create_default_context(
|
|
60
|
+
cafile=configuration.ssl_ca_cert,
|
|
61
|
+
cadata=configuration.ca_cert_data,
|
|
62
|
+
)
|
|
63
|
+
if configuration.cert_file:
|
|
64
|
+
self.ssl_context.load_cert_chain(
|
|
65
|
+
configuration.cert_file, keyfile=configuration.key_file
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not configuration.verify_ssl:
|
|
69
|
+
self.ssl_context.check_hostname = False
|
|
70
|
+
self.ssl_context.verify_mode = ssl.CERT_NONE
|
|
71
|
+
|
|
72
|
+
self.proxy = configuration.proxy
|
|
73
|
+
self.proxy_headers = configuration.proxy_headers
|
|
74
|
+
|
|
75
|
+
self.retries = configuration.retries
|
|
76
|
+
|
|
77
|
+
self.pool_manager: Optional[aiohttp.ClientSession] = None
|
|
78
|
+
self.retry_client: Optional[aiohttp_retry.RetryClient] = None
|
|
79
|
+
|
|
80
|
+
async def close(self) -> None:
|
|
81
|
+
if self.pool_manager:
|
|
82
|
+
await self.pool_manager.close()
|
|
83
|
+
if self.retry_client is not None:
|
|
84
|
+
await self.retry_client.close()
|
|
85
|
+
|
|
86
|
+
async def request(
|
|
87
|
+
self,
|
|
88
|
+
method,
|
|
89
|
+
url,
|
|
90
|
+
headers=None,
|
|
91
|
+
body=None,
|
|
92
|
+
post_params=None,
|
|
93
|
+
_request_timeout=None
|
|
94
|
+
):
|
|
95
|
+
"""Execute request
|
|
96
|
+
|
|
97
|
+
:param method: http request method
|
|
98
|
+
:param url: http request url
|
|
99
|
+
:param headers: http request headers
|
|
100
|
+
:param body: request json body, for `application/json`
|
|
101
|
+
:param post_params: request post parameters,
|
|
102
|
+
`application/x-www-form-urlencoded`
|
|
103
|
+
and `multipart/form-data`
|
|
104
|
+
:param _request_timeout: timeout setting for this request. If one
|
|
105
|
+
number provided, it will be total request
|
|
106
|
+
timeout. It can also be a pair (tuple) of
|
|
107
|
+
(connection, read) timeouts.
|
|
108
|
+
"""
|
|
109
|
+
method = method.upper()
|
|
110
|
+
assert method in [
|
|
111
|
+
'GET',
|
|
112
|
+
'HEAD',
|
|
113
|
+
'DELETE',
|
|
114
|
+
'POST',
|
|
115
|
+
'PUT',
|
|
116
|
+
'PATCH',
|
|
117
|
+
'OPTIONS'
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
if post_params and body:
|
|
121
|
+
raise ApiValueError(
|
|
122
|
+
"body parameter cannot be used with post_params parameter."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
post_params = post_params or {}
|
|
126
|
+
headers = headers or {}
|
|
127
|
+
# url already contains the URL query string
|
|
128
|
+
timeout = _request_timeout or 5 * 60
|
|
129
|
+
|
|
130
|
+
if 'Content-Type' not in headers:
|
|
131
|
+
headers['Content-Type'] = 'application/json'
|
|
132
|
+
|
|
133
|
+
args = {
|
|
134
|
+
"method": method,
|
|
135
|
+
"url": url,
|
|
136
|
+
"timeout": timeout,
|
|
137
|
+
"headers": headers
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if self.proxy:
|
|
141
|
+
args["proxy"] = self.proxy
|
|
142
|
+
if self.proxy_headers:
|
|
143
|
+
args["proxy_headers"] = self.proxy_headers
|
|
144
|
+
|
|
145
|
+
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
|
|
146
|
+
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
|
|
147
|
+
if re.search('json', headers['Content-Type'], re.IGNORECASE):
|
|
148
|
+
if body is not None:
|
|
149
|
+
body = json.dumps(body)
|
|
150
|
+
args["data"] = body
|
|
151
|
+
elif headers['Content-Type'] == 'application/x-www-form-urlencoded':
|
|
152
|
+
args["data"] = aiohttp.FormData(post_params)
|
|
153
|
+
elif headers['Content-Type'] == 'multipart/form-data':
|
|
154
|
+
# must del headers['Content-Type'], or the correct
|
|
155
|
+
# Content-Type which generated by aiohttp
|
|
156
|
+
del headers['Content-Type']
|
|
157
|
+
data = aiohttp.FormData()
|
|
158
|
+
for param in post_params:
|
|
159
|
+
k, v = param
|
|
160
|
+
if isinstance(v, tuple) and len(v) == 3:
|
|
161
|
+
data.add_field(
|
|
162
|
+
k,
|
|
163
|
+
value=v[1],
|
|
164
|
+
filename=v[0],
|
|
165
|
+
content_type=v[2]
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
# Ensures that dict objects are serialized
|
|
169
|
+
if isinstance(v, dict):
|
|
170
|
+
v = json.dumps(v)
|
|
171
|
+
elif isinstance(v, int):
|
|
172
|
+
v = str(v)
|
|
173
|
+
data.add_field(k, v)
|
|
174
|
+
args["data"] = data
|
|
175
|
+
|
|
176
|
+
# Pass a `bytes` or `str` parameter directly in the body to support
|
|
177
|
+
# other content types than Json when `body` argument is provided
|
|
178
|
+
# in serialized form
|
|
179
|
+
elif isinstance(body, str) or isinstance(body, bytes):
|
|
180
|
+
args["data"] = body
|
|
181
|
+
else:
|
|
182
|
+
# Cannot generate the request from given parameters
|
|
183
|
+
msg = """Cannot prepare a request message for provided
|
|
184
|
+
arguments. Please check that your arguments match
|
|
185
|
+
declared content type."""
|
|
186
|
+
raise ApiException(status=0, reason=msg)
|
|
187
|
+
|
|
188
|
+
pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient]
|
|
189
|
+
|
|
190
|
+
# https pool manager
|
|
191
|
+
if self.pool_manager is None:
|
|
192
|
+
self.pool_manager = aiohttp.ClientSession(
|
|
193
|
+
connector=aiohttp.TCPConnector(limit=self.maxsize, ssl=self.ssl_context),
|
|
194
|
+
trust_env=True,
|
|
195
|
+
)
|
|
196
|
+
pool_manager = self.pool_manager
|
|
197
|
+
|
|
198
|
+
if self.retries is not None and method in ALLOW_RETRY_METHODS:
|
|
199
|
+
if self.retry_client is None:
|
|
200
|
+
self.retry_client = aiohttp_retry.RetryClient(
|
|
201
|
+
client_session=self.pool_manager,
|
|
202
|
+
retry_options=aiohttp_retry.ExponentialRetry(
|
|
203
|
+
attempts=self.retries,
|
|
204
|
+
factor=2.0,
|
|
205
|
+
start_timeout=0.1,
|
|
206
|
+
max_timeout=120.0
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
pool_manager = self.retry_client
|
|
210
|
+
|
|
211
|
+
r = await pool_manager.request(**args)
|
|
212
|
+
|
|
213
|
+
return RESTResponse(r)
|
qi2_shared/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Tuple, cast
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from qi2_shared.settings import ApiSettings, TokenInfo, Url
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthorisationError(Exception):
|
|
10
|
+
"""Indicates that the authorisation permanently went wrong."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IdentityProvider:
|
|
16
|
+
"""Class for interfacing with the IdentityProvider."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, well_known_endpoint: str):
|
|
19
|
+
self._well_known_endpoint = well_known_endpoint
|
|
20
|
+
self._token_endpoint, self._device_endpoint = self._get_endpoints()
|
|
21
|
+
self._headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
22
|
+
|
|
23
|
+
def _get_endpoints(self) -> Tuple[str, str]:
|
|
24
|
+
response = requests.get(self._well_known_endpoint)
|
|
25
|
+
response.raise_for_status()
|
|
26
|
+
config = response.json()
|
|
27
|
+
return config["token_endpoint"], config["device_authorization_endpoint"]
|
|
28
|
+
|
|
29
|
+
def refresh_access_token(self, client_id: str, refresh_token: str) -> dict[str, Any]:
|
|
30
|
+
data = {
|
|
31
|
+
"grant_type": "refresh_token",
|
|
32
|
+
"client_id": client_id,
|
|
33
|
+
"refresh_token": refresh_token,
|
|
34
|
+
}
|
|
35
|
+
response = requests.post(self._token_endpoint, headers=self._headers, data=data)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
return cast(dict[str, Any], response.json())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OauthDeviceSession:
|
|
41
|
+
"""Class for storing OAuth session information and refreshing tokens when needed."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, host: Url, settings: ApiSettings, identity_provider: IdentityProvider):
|
|
44
|
+
self._api_settings = settings
|
|
45
|
+
_auth_settings = settings.auths[host]
|
|
46
|
+
self._host = host
|
|
47
|
+
self._client_id = _auth_settings.client_id
|
|
48
|
+
self._token_info = _auth_settings.tokens
|
|
49
|
+
self._refresh_time_reduction = 5 # the number of seconds to refresh the expiration time
|
|
50
|
+
self._identity_provider = identity_provider
|
|
51
|
+
|
|
52
|
+
def refresh(self) -> TokenInfo:
|
|
53
|
+
if self._token_info is None:
|
|
54
|
+
raise AuthorisationError("You should authenticate first before you can refresh")
|
|
55
|
+
|
|
56
|
+
if self._token_info.access_expires_at > time.time() + self._refresh_time_reduction:
|
|
57
|
+
return self._token_info
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
self._token_info = TokenInfo(
|
|
61
|
+
**self._identity_provider.refresh_access_token(self._client_id, self._token_info.refresh_token)
|
|
62
|
+
)
|
|
63
|
+
self._api_settings.store_tokens(self._host, self._token_info)
|
|
64
|
+
return self._token_info
|
|
65
|
+
except requests.HTTPError as e:
|
|
66
|
+
raise AuthorisationError(f"An error occurred during token refresh: {e}")
|
qi2_shared/client.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
import compute_api_client
|
|
4
|
+
|
|
5
|
+
from qi2_shared.authentication import IdentityProvider, OauthDeviceSession
|
|
6
|
+
from qi2_shared.settings import ApiSettings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Configuration(compute_api_client.Configuration): # type: ignore[misc]
|
|
10
|
+
"""Original Configuration class in compute_api_client does not handle refreshing bearer tokens, so we need to add
|
|
11
|
+
some functionality."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, host: str, oauth_session: OauthDeviceSession, **kwargs: Any):
|
|
14
|
+
self._oauth_session = oauth_session
|
|
15
|
+
super().__init__(host=host, **kwargs)
|
|
16
|
+
|
|
17
|
+
def auth_settings(self) -> Any:
|
|
18
|
+
token_info = self._oauth_session.refresh()
|
|
19
|
+
self.access_token = token_info.access_token
|
|
20
|
+
return super().auth_settings()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_config: Optional[Configuration] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def connect() -> None:
|
|
27
|
+
"""Set connection configuration for the Quantum Inspire API.
|
|
28
|
+
|
|
29
|
+
Call after logging in with the CLI. Will remove old configuration.
|
|
30
|
+
"""
|
|
31
|
+
global _config
|
|
32
|
+
settings = ApiSettings.from_config_file()
|
|
33
|
+
|
|
34
|
+
tokens = settings.auths[settings.default_host].tokens
|
|
35
|
+
|
|
36
|
+
if tokens is None:
|
|
37
|
+
raise ValueError("No access token found for the default host. Please connect to Quantum Inspire using the CLI.")
|
|
38
|
+
|
|
39
|
+
host = settings.default_host
|
|
40
|
+
_config = Configuration(
|
|
41
|
+
host=host,
|
|
42
|
+
oauth_session=OauthDeviceSession(host, settings, IdentityProvider(settings.auths[host].well_known_endpoint)),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def config() -> Configuration:
|
|
47
|
+
global _config
|
|
48
|
+
if _config is None:
|
|
49
|
+
connect()
|
|
50
|
+
|
|
51
|
+
assert _config is not None
|
|
52
|
+
return _config
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Quantum Inspire SDK.
|
|
2
|
+
|
|
3
|
+
Copyright 2022 QuTech Delft
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
|
|
6
|
+
License. You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
|
|
11
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
|
|
12
|
+
language governing permissions and limitations under the License.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from compute_api_client.models.backend_type import BackendType
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExecuteCircuitResult(BaseModel):
|
|
23
|
+
"""Result of executing a quantum circuit."""
|
|
24
|
+
|
|
25
|
+
results: Dict[str, int]
|
|
26
|
+
raw_data: Optional[List[str]]
|
|
27
|
+
shots_requested: int
|
|
28
|
+
shots_done: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class QuantumInterface(ABC):
|
|
32
|
+
"""Interface for running quantum circuits from hybrid algorithms."""
|
|
33
|
+
|
|
34
|
+
# pylint: disable = R0903
|
|
35
|
+
# Too few public methods (1/2) (too-few-public-methods)
|
|
36
|
+
|
|
37
|
+
results: List[Dict[str, Any]]
|
|
38
|
+
backend_type: BackendType
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def execute_circuit(
|
|
42
|
+
self, circuit: str, number_of_shots: int, raw_data_enabled: bool = False
|
|
43
|
+
) -> ExecuteCircuitResult:
|
|
44
|
+
"""Execute a quantum circuit."""
|
|
45
|
+
raise NotImplementedError
|
qi2_shared/pagination.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any, Awaitable, Callable, Generic, List, Optional, TypeVar, Union, cast
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing_extensions import Annotated
|
|
5
|
+
|
|
6
|
+
PageType = TypeVar("PageType")
|
|
7
|
+
ItemType = TypeVar("ItemType")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PageInterface(BaseModel, Generic[ItemType]):
|
|
11
|
+
"""The page models in the generated API client don't inherit from a common base class, so we have to trick the
|
|
12
|
+
typing system a bit with this fake base class."""
|
|
13
|
+
|
|
14
|
+
items: List[ItemType]
|
|
15
|
+
total: Optional[Annotated[int, Field(strict=True, ge=0)]]
|
|
16
|
+
page: Optional[Annotated[int, Field(strict=True, ge=1)]]
|
|
17
|
+
size: Optional[Annotated[int, Field(strict=True, ge=1)]]
|
|
18
|
+
pages: Optional[Annotated[int, Field(strict=True, ge=0)]] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PageReader(Generic[PageType, ItemType]):
|
|
22
|
+
"""Helper class for reading fastapi-pagination style pages returned by the compute_api_client."""
|
|
23
|
+
|
|
24
|
+
async def get_all(self, api_call: Callable[..., Awaitable[PageType]], **kwargs: Any) -> List[ItemType]:
|
|
25
|
+
"""Get all items from an API call that supports paging."""
|
|
26
|
+
items: List[ItemType] = []
|
|
27
|
+
page = 1
|
|
28
|
+
|
|
29
|
+
while True:
|
|
30
|
+
response = cast(PageInterface[ItemType], await api_call(page=page, **kwargs))
|
|
31
|
+
|
|
32
|
+
items.extend(response.items)
|
|
33
|
+
page += 1
|
|
34
|
+
if response.pages is None or page > response.pages:
|
|
35
|
+
break
|
|
36
|
+
return items
|
|
37
|
+
|
|
38
|
+
async def get_single(self, api_call: Callable[..., Awaitable[PageType]], **kwargs: Any) -> Union[ItemType, None]:
|
|
39
|
+
"""Get a single item from an API call that supports paging."""
|
|
40
|
+
response = cast(PageInterface[ItemType], await api_call(**kwargs))
|
|
41
|
+
if len(response.items) > 1:
|
|
42
|
+
raise RuntimeError(f"Response contains more than one item -> {kwargs}.")
|
|
43
|
+
|
|
44
|
+
return response.items[0] if response.items else None
|
qi2_shared/settings.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Module containing the handler for the Quantum Inspire persistent configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, BeforeValidator, Field, HttpUrl
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
Url = Annotated[str, BeforeValidator(lambda value: str(HttpUrl(value)).rstrip("/"))]
|
|
13
|
+
API_SETTINGS_FILE = Path.joinpath(Path.home(), ".quantuminspire", "config.json")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TokenInfo(BaseModel):
|
|
17
|
+
"""A pydantic model for storing all information regarding oauth access and refresh tokens."""
|
|
18
|
+
|
|
19
|
+
access_token: str
|
|
20
|
+
expires_in: int # [s]
|
|
21
|
+
refresh_token: str
|
|
22
|
+
refresh_expires_in: Optional[int] = None # [s]
|
|
23
|
+
generated_at: float = Field(default_factory=time.time)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def access_expires_at(self) -> float:
|
|
27
|
+
"""Unix timestamp containing the time when the access token will expire."""
|
|
28
|
+
return self.generated_at + self.expires_in
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthSettings(BaseModel):
|
|
32
|
+
"""Pydantic model for storing all auth related settings for a given host."""
|
|
33
|
+
|
|
34
|
+
client_id: str
|
|
35
|
+
code_challenge_method: str
|
|
36
|
+
code_verifyer_length: int
|
|
37
|
+
well_known_endpoint: Url
|
|
38
|
+
tokens: Optional[TokenInfo]
|
|
39
|
+
team_member_id: Optional[int]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ApiSettings(BaseModel):
|
|
43
|
+
"""The settings class for the Quantum Inspire persistent configuration."""
|
|
44
|
+
|
|
45
|
+
auths: Dict[Url, AuthSettings]
|
|
46
|
+
default_host: Url
|
|
47
|
+
|
|
48
|
+
def store_tokens(self, host: Url, tokens: TokenInfo, path: Path = API_SETTINGS_FILE) -> None:
|
|
49
|
+
"""Stores the team_member_id, access and refresh tokens in the config.json file.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
host: The hostname of the API for which the tokens are intended.
|
|
53
|
+
tokens: OAuth access and refresh tokens.
|
|
54
|
+
path: The path to the config.json file. Defaults to API_SETTINGS_FILE.
|
|
55
|
+
Returns:
|
|
56
|
+
None
|
|
57
|
+
"""
|
|
58
|
+
self.auths[host].tokens = tokens
|
|
59
|
+
path.write_text(self.model_dump_json(indent=2))
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_config_file(cls, path: Path = API_SETTINGS_FILE) -> ApiSettings:
|
|
63
|
+
"""Load the configuration from a file."""
|
|
64
|
+
if not path.is_file():
|
|
65
|
+
raise FileNotFoundError("No configuration file found. Please connect to Quantum Inspire using the CLI.")
|
|
66
|
+
|
|
67
|
+
api_settings = path.read_text()
|
|
68
|
+
return ApiSettings.model_validate_json(api_settings)
|
qi2_shared/utils.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import concurrent
|
|
3
|
+
from typing import Any, Coroutine
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_async(async_function: Coroutine[Any, Any, Any]) -> Any:
|
|
7
|
+
try:
|
|
8
|
+
_ = asyncio.get_running_loop()
|
|
9
|
+
except RuntimeError:
|
|
10
|
+
return asyncio.run(async_function)
|
|
11
|
+
|
|
12
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
13
|
+
return executor.submit(asyncio.run, async_function).result()
|