quantum-flows 0.1.19__tar.gz → 0.1.21__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.
- {quantum_flows-0.1.19 → quantum_flows-0.1.21}/PKG-INFO +1 -1
- {quantum_flows-0.1.19 → quantum_flows-0.1.21}/pyproject.toml +1 -1
- quantum_flows-0.1.19/quantum_flows/quantum_flows.py.bck → quantum_flows-0.1.21/quantum_flows/quantum_flows.py +525 -220
- {quantum_flows-0.1.19 → quantum_flows-0.1.21}/LICENSE +0 -0
- {quantum_flows-0.1.19 → quantum_flows-0.1.21}/README.md +0 -0
- {quantum_flows-0.1.19 → quantum_flows-0.1.21}/quantum_flows/__init__.py +0 -0
- /quantum_flows-0.1.19/quantum_flows/quantum_flows.py → /quantum_flows-0.1.21/quantum_flows/quantum_flows.py.bck +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quantum-flows"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.21"
|
|
4
4
|
description = "A python library for interacting with Transilvania-Quantum Quantum Flows quantum computing API backbone."
|
|
5
5
|
authors = ["Radu Marginean <radu.marginean@transilvania-quantum.com>"]
|
|
6
6
|
license='MIT'
|
|
@@ -6,9 +6,11 @@ import qiskit
|
|
|
6
6
|
import requests
|
|
7
7
|
import secrets
|
|
8
8
|
import time
|
|
9
|
+
import traceback
|
|
9
10
|
import uuid
|
|
10
11
|
import webbrowser
|
|
11
12
|
|
|
13
|
+
from collections.abc import Sequence
|
|
12
14
|
from IPython.display import display, HTML
|
|
13
15
|
from keycloak import KeycloakOpenID
|
|
14
16
|
from urllib.parse import urlencode
|
|
@@ -16,7 +18,6 @@ from urllib.parse import urlencode
|
|
|
16
18
|
from qiskit import qpy
|
|
17
19
|
from qiskit import QuantumCircuit
|
|
18
20
|
from qiskit.quantum_info import Operator, Pauli, PauliList, SparsePauliOp
|
|
19
|
-
from qiskit.quantum_info.operators.linear_op import LinearOp
|
|
20
21
|
from qiskit_nature.second_q.hamiltonians.lattices import (
|
|
21
22
|
KagomeLattice,
|
|
22
23
|
Lattice,
|
|
@@ -32,20 +33,40 @@ from qiskit_nature.second_q.hamiltonians.lattices.boundary_condition import (
|
|
|
32
33
|
from qiskit_optimization import QuadraticProgram
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
DEFAULT_TIMEOUT = (3.05, 10) # (connect timeout, read timeout)
|
|
37
|
+
|
|
38
|
+
|
|
35
39
|
class CustomJSONEncoder(json.JSONEncoder):
|
|
36
40
|
def default(self, obj):
|
|
41
|
+
if isinstance(obj, np.generic):
|
|
42
|
+
if np.iscomplexobj(obj):
|
|
43
|
+
c = complex(obj)
|
|
44
|
+
return {"real": c.real, "imag": c.imag}
|
|
45
|
+
return obj.item()
|
|
37
46
|
if isinstance(obj, complex):
|
|
38
47
|
return {"real": obj.real, "imag": obj.imag}
|
|
39
48
|
if isinstance(obj, np.ndarray):
|
|
40
49
|
return obj.tolist()
|
|
41
50
|
if isinstance(obj, BoundaryCondition):
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
return obj.name
|
|
52
|
+
if isinstance(obj, (set, frozenset)):
|
|
53
|
+
return list(obj)
|
|
54
|
+
if isinstance(obj, tuple):
|
|
55
|
+
return list(obj)
|
|
56
|
+
|
|
44
57
|
return super().default(obj)
|
|
45
58
|
|
|
46
59
|
|
|
47
|
-
def
|
|
48
|
-
return
|
|
60
|
+
def _is_number(x) -> bool:
|
|
61
|
+
return isinstance(x, (int, float, complex, np.generic))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _to_complex_dict(c):
|
|
65
|
+
if isinstance(c, np.generic):
|
|
66
|
+
c = c.item()
|
|
67
|
+
if isinstance(c, complex):
|
|
68
|
+
return {"real": float(c.real), "imag": float(c.imag)}
|
|
69
|
+
return {"real": float(c), "imag": 0.0}
|
|
49
70
|
|
|
50
71
|
|
|
51
72
|
def serialize_circuit(circuit):
|
|
@@ -57,13 +78,11 @@ def serialize_circuit(circuit):
|
|
|
57
78
|
|
|
58
79
|
|
|
59
80
|
class AuthenticationFailure(Exception):
|
|
60
|
-
|
|
61
|
-
self.message = message
|
|
81
|
+
pass
|
|
62
82
|
|
|
63
83
|
|
|
64
84
|
class AuthorizationFailure(Exception):
|
|
65
|
-
|
|
66
|
-
self.message = message
|
|
85
|
+
pass
|
|
67
86
|
|
|
68
87
|
|
|
69
88
|
class Job:
|
|
@@ -88,18 +107,44 @@ class InputData:
|
|
|
88
107
|
if label:
|
|
89
108
|
self.add_data(label, content)
|
|
90
109
|
|
|
110
|
+
def _assert_jsonable(self, value, label: str):
|
|
111
|
+
try:
|
|
112
|
+
json.dumps(value, cls=CustomJSONEncoder)
|
|
113
|
+
except (TypeError, OverflowError, ValueError) as e:
|
|
114
|
+
raise Exception(
|
|
115
|
+
f"Input data '{label}' is not JSON-serializable: {e}"
|
|
116
|
+
) from e
|
|
117
|
+
|
|
91
118
|
def __str__(self):
|
|
92
|
-
|
|
119
|
+
try:
|
|
120
|
+
return json.dumps(self.data, indent=2, cls=CustomJSONEncoder)
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
raise Exception(f"Invalid input data format: {e}")
|
|
123
|
+
except (OverflowError, TypeError) as e:
|
|
124
|
+
raise Exception(f"Input data content must be JSON serializable: {e}.")
|
|
125
|
+
|
|
126
|
+
def _normalize_operator_input(self, content):
|
|
127
|
+
if isinstance(content, tuple):
|
|
128
|
+
if len(content) != 2:
|
|
129
|
+
raise Exception(
|
|
130
|
+
"Operator tuple must be exactly (PauliList, coeffs_or_none)."
|
|
131
|
+
)
|
|
132
|
+
op, coeffs = content
|
|
133
|
+
if not isinstance(op, PauliList):
|
|
134
|
+
raise Exception(
|
|
135
|
+
"Operator tuple is only supported for (PauliList, coeffs_or_none)."
|
|
136
|
+
)
|
|
137
|
+
return op, coeffs
|
|
138
|
+
return content, None
|
|
93
139
|
|
|
94
140
|
def add_data(self, label, content):
|
|
95
141
|
self.check_label(label, self.data)
|
|
96
142
|
try:
|
|
97
143
|
if label == "operator":
|
|
98
|
-
operator = content
|
|
99
|
-
self.validate_operator(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
operator, coeffs = content
|
|
144
|
+
operator, coeffs = self._normalize_operator_input(content)
|
|
145
|
+
self.validate_operator(
|
|
146
|
+
(operator, coeffs) if coeffs is not None else operator
|
|
147
|
+
)
|
|
103
148
|
sparse_pauli_operator = self.to_sparse_pauli_operator(
|
|
104
149
|
operator, coeffs=coeffs
|
|
105
150
|
)
|
|
@@ -111,36 +156,48 @@ class InputData:
|
|
|
111
156
|
"coefficients": coefficients,
|
|
112
157
|
"operator-string-representation": str(operator),
|
|
113
158
|
}
|
|
114
|
-
elif label == "
|
|
159
|
+
elif label == "pubs":
|
|
115
160
|
content = self.validate_and_serialize_pub(content)
|
|
116
|
-
if
|
|
161
|
+
if "pubs" not in self.data:
|
|
117
162
|
self.data["pubs"] = []
|
|
163
|
+
self._assert_jsonable(content, label)
|
|
118
164
|
self.data["pubs"].append(content)
|
|
119
165
|
elif label == "molecule-info":
|
|
120
166
|
self.validate_molecule_info(content)
|
|
167
|
+
self._assert_jsonable(content, label)
|
|
121
168
|
self.data[label] = content
|
|
122
169
|
elif label == "lattice":
|
|
123
|
-
|
|
170
|
+
lattice_dict = self.lattice_to_dict(content)
|
|
171
|
+
self._assert_jsonable(lattice_dict, label)
|
|
172
|
+
self.data[label] = lattice_dict
|
|
124
173
|
elif label == "ising-model":
|
|
125
174
|
self.validate_ising_model(content)
|
|
175
|
+
self._assert_jsonable(content, label)
|
|
126
176
|
self.data[label] = content
|
|
127
177
|
elif label == "training-data":
|
|
128
178
|
self.validate_training_data(content)
|
|
179
|
+
self._assert_jsonable(content, label)
|
|
129
180
|
self.data[label] = content
|
|
130
181
|
elif label == "inference-data":
|
|
131
182
|
self.validate_inference_data(content)
|
|
183
|
+
self._assert_jsonable(content, label)
|
|
132
184
|
self.data[label] = content
|
|
133
185
|
elif label == "quadratic-program":
|
|
134
186
|
self.validate_quadratic_program(content)
|
|
135
187
|
lp_string = content.export_as_lp_string()
|
|
188
|
+
self._assert_jsonable(lp_string, label)
|
|
136
189
|
self.data[label] = lp_string
|
|
137
190
|
else:
|
|
191
|
+
self._assert_jsonable(content, label)
|
|
138
192
|
self.data[label] = content
|
|
139
|
-
|
|
140
|
-
|
|
193
|
+
|
|
194
|
+
except ValueError as e:
|
|
195
|
+
raise Exception(f"Invalid input data format: {e}")
|
|
196
|
+
except (OverflowError, TypeError) as e:
|
|
197
|
+
raise Exception(f"Input data content must be JSON serializable: {e}")
|
|
141
198
|
|
|
142
199
|
def check_label(self, label, data):
|
|
143
|
-
if
|
|
200
|
+
if not isinstance(label, str):
|
|
144
201
|
raise Exception("Input data label must be string.")
|
|
145
202
|
if label not in [
|
|
146
203
|
"ansatz-parameters",
|
|
@@ -148,17 +205,17 @@ class InputData:
|
|
|
148
205
|
"ising-model",
|
|
149
206
|
"lattice",
|
|
150
207
|
"lp-model",
|
|
151
|
-
"max-
|
|
208
|
+
"max-fun-evaluations",
|
|
152
209
|
"molecule-info",
|
|
153
210
|
"operator",
|
|
154
|
-
"
|
|
211
|
+
"pubs",
|
|
155
212
|
"quadratic-program",
|
|
156
213
|
"training-data",
|
|
157
214
|
]:
|
|
158
215
|
raise Exception(
|
|
159
|
-
f"Input data of type {label} is not supported. Please choose one of the following options: 'ansatz-parameters', 'inference-data' 'ising-model', 'lattice', 'lp-model', 'molecule-info', 'operator', 'pub', 'training-data'."
|
|
216
|
+
f"Input data of type {label} is not supported. Please choose one of the following options: 'ansatz-parameters', 'inference-data' 'ising-model', 'lattice', 'lp-model', 'molecule-info', 'operator', 'pub', 'pubs', 'training-data'."
|
|
160
217
|
)
|
|
161
|
-
if label != "
|
|
218
|
+
if label != "pubs" and label in data.keys():
|
|
162
219
|
raise Exception(
|
|
163
220
|
f"An input data item of type '{label}' has already been added to the job input data. Multiple data items of same category are allowed only for PUBs."
|
|
164
221
|
)
|
|
@@ -168,7 +225,14 @@ class InputData:
|
|
|
168
225
|
lattice_data = {
|
|
169
226
|
"type": "LineLattice",
|
|
170
227
|
"num_nodes": lattice.num_nodes,
|
|
171
|
-
"boundary_condition":
|
|
228
|
+
"boundary_condition": [
|
|
229
|
+
bc.name
|
|
230
|
+
for bc in (
|
|
231
|
+
lattice.boundary_condition
|
|
232
|
+
if isinstance(lattice.boundary_condition, tuple)
|
|
233
|
+
else (lattice.boundary_condition,)
|
|
234
|
+
)
|
|
235
|
+
],
|
|
172
236
|
"edge_parameter": lattice.edge_parameter,
|
|
173
237
|
"onsite_parameter": lattice.onsite_parameter,
|
|
174
238
|
}
|
|
@@ -178,7 +242,14 @@ class InputData:
|
|
|
178
242
|
"type": "TriangularLattice",
|
|
179
243
|
"rows": lattice.rows,
|
|
180
244
|
"cols": lattice.cols,
|
|
181
|
-
"boundary_condition":
|
|
245
|
+
"boundary_condition": [
|
|
246
|
+
bc.name
|
|
247
|
+
for bc in (
|
|
248
|
+
lattice.boundary_condition
|
|
249
|
+
if isinstance(lattice.boundary_condition, tuple)
|
|
250
|
+
else (lattice.boundary_condition,)
|
|
251
|
+
)
|
|
252
|
+
],
|
|
182
253
|
"edge_parameter": lattice.edge_parameter,
|
|
183
254
|
"onsite_parameter": lattice.onsite_parameter,
|
|
184
255
|
}
|
|
@@ -229,14 +300,31 @@ class InputData:
|
|
|
229
300
|
)
|
|
230
301
|
|
|
231
302
|
def validate_molecule_info(self, molecule_info):
|
|
232
|
-
if not isinstance(molecule_info
|
|
233
|
-
raise Exception("The '
|
|
234
|
-
if
|
|
303
|
+
if not isinstance(molecule_info, dict):
|
|
304
|
+
raise Exception("The 'molecule_info' must be a dictionary.")
|
|
305
|
+
if (
|
|
306
|
+
"symbols" not in molecule_info
|
|
307
|
+
or not isinstance(molecule_info["symbols"], list)
|
|
308
|
+
or not all(isinstance(x, str) for x in molecule_info["symbols"])
|
|
309
|
+
):
|
|
310
|
+
raise Exception("Add 'symbols' as a list of nuclei name strings.")
|
|
311
|
+
if (
|
|
312
|
+
"coords" not in molecule_info
|
|
313
|
+
or not isinstance(molecule_info["coords"], list)
|
|
314
|
+
or len(molecule_info["coords"]) != len(molecule_info["symbols"])
|
|
315
|
+
or not all(
|
|
316
|
+
isinstance(c, tuple) and len(c) == 3 for c in molecule_info["coords"]
|
|
317
|
+
)
|
|
318
|
+
or not all(
|
|
319
|
+
all(isinstance(i, (int, float)) for i in x)
|
|
320
|
+
for x in molecule_info["coords"]
|
|
321
|
+
)
|
|
322
|
+
):
|
|
235
323
|
raise Exception(
|
|
236
|
-
"The 'coords' must be a list of tuples representing the x, y, z position of each nuclei."
|
|
324
|
+
"The 'coords' must be a list of tuples with numbers representing the x, y, z position of each nuclei."
|
|
237
325
|
)
|
|
238
|
-
if "
|
|
239
|
-
molecule_info["
|
|
326
|
+
if "multiplicity" in molecule_info and not isinstance(
|
|
327
|
+
molecule_info["multiplicity"], int
|
|
240
328
|
):
|
|
241
329
|
raise Exception("The 'multiplicity' must be an integer.")
|
|
242
330
|
if "charge" in molecule_info and not isinstance(molecule_info["charge"], int):
|
|
@@ -247,12 +335,17 @@ class InputData:
|
|
|
247
335
|
and molecule_info["units"].lower() != "bohr"
|
|
248
336
|
):
|
|
249
337
|
raise Exception("The 'units' must be either 'Angstrom' or 'Bohr'.")
|
|
250
|
-
if "masses" in molecule_info
|
|
251
|
-
isinstance(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
338
|
+
if "masses" in molecule_info:
|
|
339
|
+
if not isinstance(molecule_info["masses"], list):
|
|
340
|
+
raise Exception("The 'masses' must be a list of numbers.")
|
|
341
|
+
if not all(isinstance(m, (int, float)) for m in molecule_info["masses"]):
|
|
342
|
+
raise Exception(
|
|
343
|
+
"The 'masses' must be a list of numbers, one for each nucleus in the molecule."
|
|
344
|
+
)
|
|
345
|
+
if len(molecule_info["masses"]) != len(molecule_info["symbols"]):
|
|
346
|
+
raise Exception(
|
|
347
|
+
"The 'masses' list must have the same length as the 'symbols' list."
|
|
348
|
+
)
|
|
256
349
|
|
|
257
350
|
def validate_ising_model(self, ising_model):
|
|
258
351
|
if not isinstance(ising_model, dict):
|
|
@@ -293,9 +386,14 @@ class InputData:
|
|
|
293
386
|
|
|
294
387
|
def validate_training_data(self, training_data):
|
|
295
388
|
vector_size = None
|
|
389
|
+
is_classification = False
|
|
390
|
+
is_regression = False
|
|
391
|
+
line = 0
|
|
392
|
+
output_length = None
|
|
296
393
|
if not isinstance(training_data, list):
|
|
297
394
|
raise Exception("The 'training_data' must be a list of dictionaries.")
|
|
298
395
|
for data in training_data:
|
|
396
|
+
line += 1
|
|
299
397
|
if not isinstance(data, dict):
|
|
300
398
|
raise Exception("The 'training_data' must be a list of dictionaries.")
|
|
301
399
|
if not "data-point" in data:
|
|
@@ -310,11 +408,11 @@ class InputData:
|
|
|
310
408
|
)
|
|
311
409
|
if data_tags is not None and not isinstance(data_tags, list):
|
|
312
410
|
raise Exception(
|
|
313
|
-
"The optional 'data-tags' value must be a list of strings."
|
|
411
|
+
f"The optional 'data-tags' value must be a list of strings (check line {line})."
|
|
314
412
|
)
|
|
315
413
|
if not all(isinstance(item, (int, float)) for item in vector):
|
|
316
414
|
raise Exception(
|
|
317
|
-
"The 'data-point' value must be a list of numeric values (int or float)."
|
|
415
|
+
f"The 'data-point' value must be a list of numeric values (int or float) (check line {line})."
|
|
318
416
|
)
|
|
319
417
|
if vector_size is None:
|
|
320
418
|
vector_size = len(vector)
|
|
@@ -324,16 +422,46 @@ class InputData:
|
|
|
324
422
|
)
|
|
325
423
|
if data_tags is not None and len(data_tags) != vector_size:
|
|
326
424
|
raise Exception(
|
|
327
|
-
"If provided, the 'data-tags' list must have the same length as the 'data-point' vector."
|
|
425
|
+
f"If provided, the 'data-tags' list must have the same length as the 'data-point' vector (check line {line})."
|
|
328
426
|
)
|
|
329
|
-
if not
|
|
427
|
+
if data_tags is not None and not all(
|
|
428
|
+
isinstance(tag, str) for tag in data_tags
|
|
429
|
+
):
|
|
330
430
|
raise Exception(
|
|
331
|
-
"
|
|
431
|
+
f"If provided, the 'data-tags' list must contain only strings (check line {line})."
|
|
332
432
|
)
|
|
333
|
-
label
|
|
334
|
-
|
|
433
|
+
if "label" in data:
|
|
434
|
+
is_classification = True
|
|
435
|
+
label = data["label"]
|
|
436
|
+
if not isinstance(label, int):
|
|
437
|
+
raise Exception(
|
|
438
|
+
f"The 'label' value must be an integer (check line {line})."
|
|
439
|
+
)
|
|
440
|
+
if "output" in data:
|
|
441
|
+
is_regression = True
|
|
442
|
+
output = data["output"]
|
|
443
|
+
if not isinstance(output, (int, float, list)) or (
|
|
444
|
+
isinstance(output, list)
|
|
445
|
+
and not all(isinstance(value, (int, float)) for value in output)
|
|
446
|
+
):
|
|
447
|
+
raise Exception(
|
|
448
|
+
f"The 'output' value must be a numeric type (int, float) or a list of numeric values (check line {line})."
|
|
449
|
+
)
|
|
450
|
+
values = output if isinstance(output, list) else [output]
|
|
451
|
+
for value in values:
|
|
452
|
+
if value < -1 or value > 1:
|
|
453
|
+
raise Exception(
|
|
454
|
+
f"The 'output' numeric values must be in [-1, 1] range (check line {line})."
|
|
455
|
+
)
|
|
456
|
+
if output_length is None:
|
|
457
|
+
output_length = len(values)
|
|
458
|
+
elif output_length != len(values):
|
|
459
|
+
raise Exception(
|
|
460
|
+
f"All 'output' lists in training data entries must have the same length. Choose either a consistent sized list or a numeric value (check line {line})."
|
|
461
|
+
)
|
|
462
|
+
if is_classification and is_regression:
|
|
335
463
|
raise Exception(
|
|
336
|
-
"The 'label'
|
|
464
|
+
"The training data cannot contain both 'label' and 'output' keys. Please choose either classification or regression data template."
|
|
337
465
|
)
|
|
338
466
|
|
|
339
467
|
def validate_inference_data(self, inference_data):
|
|
@@ -353,10 +481,15 @@ class InputData:
|
|
|
353
481
|
raise Exception(
|
|
354
482
|
"The 'data-point' value must be a list of numeric values."
|
|
355
483
|
)
|
|
356
|
-
if data_tags is not None
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
484
|
+
if data_tags is not None:
|
|
485
|
+
if not isinstance(data_tags, list):
|
|
486
|
+
raise Exception(
|
|
487
|
+
"The optional 'data-tags' value must be a list of strings."
|
|
488
|
+
)
|
|
489
|
+
if not all(isinstance(tag, str) for tag in data_tags):
|
|
490
|
+
raise Exception(
|
|
491
|
+
"If provided, 'data-tags' must contain only strings."
|
|
492
|
+
)
|
|
360
493
|
if not all(isinstance(item, (int, float)) for item in vector):
|
|
361
494
|
raise Exception(
|
|
362
495
|
"The 'data-point' value must be a list of numeric values (int or float)."
|
|
@@ -378,91 +511,106 @@ class InputData:
|
|
|
378
511
|
"The input object must be an instance of QuadraticProgram class from Qiskit Optimization module."
|
|
379
512
|
)
|
|
380
513
|
|
|
514
|
+
def _normalize_numeric_sequence(self, name: str, value):
|
|
515
|
+
if value is None:
|
|
516
|
+
return None
|
|
517
|
+
if isinstance(value, np.ndarray):
|
|
518
|
+
seq = value.ravel().tolist()
|
|
519
|
+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
520
|
+
seq = list(value)
|
|
521
|
+
else:
|
|
522
|
+
raise Exception(
|
|
523
|
+
f"'{name}' must be a numeric sequence (list/tuple/np.ndarray) or None."
|
|
524
|
+
)
|
|
525
|
+
if not all(_is_number(x) for x in seq):
|
|
526
|
+
raise Exception(f"'{name}' must contain only numbers.")
|
|
527
|
+
return seq
|
|
528
|
+
|
|
381
529
|
def validate_and_serialize_pub(self, pub):
|
|
382
530
|
shots = None
|
|
383
|
-
|
|
384
|
-
|
|
531
|
+
parameters = None
|
|
532
|
+
|
|
533
|
+
if isinstance(pub, QuantumCircuit):
|
|
385
534
|
quantum_circuit = pub
|
|
386
|
-
elif
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
quantum_circuit
|
|
535
|
+
elif isinstance(pub, tuple):
|
|
536
|
+
if len(pub) == 3:
|
|
537
|
+
quantum_circuit, parameters, shots = pub
|
|
538
|
+
elif len(pub) == 2:
|
|
539
|
+
quantum_circuit, parameters = pub
|
|
540
|
+
elif len(pub) == 1:
|
|
541
|
+
(quantum_circuit,) = pub
|
|
542
|
+
else:
|
|
543
|
+
raise Exception("A pub tuple must have 1..3 elements.")
|
|
544
|
+
if not isinstance(quantum_circuit, QuantumCircuit):
|
|
545
|
+
raise Exception("First element must be a QuantumCircuit.")
|
|
396
546
|
else:
|
|
397
547
|
raise Exception(
|
|
398
|
-
"A pub
|
|
399
|
-
)
|
|
400
|
-
if shots is not None and type(shots) != int:
|
|
401
|
-
raise Exception(
|
|
402
|
-
"The 'shots' setting in a PUB must be an integer and be positioned as the third element of a tuple specifying a PUB."
|
|
403
|
-
)
|
|
404
|
-
if paramaters is not None and type(paramaters) != list:
|
|
405
|
-
raise Exception(
|
|
406
|
-
"The 'paramaters' in a PUB must be a list of numbers and be positioned as the second element of a tuple specifying a PUB."
|
|
548
|
+
"A pub must be a QuantumCircuit or a tuple (circuit[, params[, shots]])."
|
|
407
549
|
)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
)
|
|
550
|
+
|
|
551
|
+
if shots is not None:
|
|
552
|
+
if not isinstance(shots, int) or shots <= 0:
|
|
553
|
+
raise Exception("'shots' must be a positive integer.")
|
|
554
|
+
|
|
555
|
+
if not isinstance(quantum_circuit, QuantumCircuit):
|
|
556
|
+
raise Exception("First element must be a QuantumCircuit.")
|
|
557
|
+
|
|
558
|
+
parameters = self._normalize_numeric_sequence("parameters", parameters)
|
|
559
|
+
|
|
560
|
+
if quantum_circuit.num_parameters == 0 and parameters not in (None, []):
|
|
417
561
|
raise Exception(
|
|
418
|
-
|
|
562
|
+
"Circuit has zero parameters; parameters must be None or []."
|
|
419
563
|
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
):
|
|
564
|
+
|
|
565
|
+
if parameters is not None and quantum_circuit.num_parameters != len(parameters):
|
|
423
566
|
raise Exception(
|
|
424
|
-
"
|
|
567
|
+
f"Parameter count mismatch: circuit expects {quantum_circuit.num_parameters}, got {len(parameters)}."
|
|
425
568
|
)
|
|
426
569
|
|
|
427
|
-
return (serialize_circuit(quantum_circuit),
|
|
570
|
+
return (serialize_circuit(quantum_circuit), parameters, shots)
|
|
428
571
|
|
|
429
572
|
def validate_operator(self, operator):
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
"The operator must be an instance of the Operator, Pauli, SparsePauliOp class or a tuple containing a PauliList and a possible empty list of numeric coefficents."
|
|
443
|
-
)
|
|
573
|
+
if isinstance(operator, (Operator, Pauli, SparsePauliOp, PauliList)):
|
|
574
|
+
if isinstance(operator, Operator):
|
|
575
|
+
matrix = operator.data
|
|
576
|
+
if not np.allclose(matrix, matrix.conj().T):
|
|
577
|
+
print("WARNING: The operator you supplied is not Hermitian!")
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
if isinstance(operator, tuple):
|
|
581
|
+
if len(operator) != 2 or not isinstance(operator[0], PauliList):
|
|
582
|
+
raise Exception(
|
|
583
|
+
"Operator tuple is only supported for (PauliList, coeffs_or_none)."
|
|
584
|
+
)
|
|
444
585
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
print("WARNING: The operator you supplied is not Hermitian!")
|
|
586
|
+
pauli_list, coeffs = operator
|
|
587
|
+
if coeffs is None:
|
|
588
|
+
return
|
|
449
589
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
and isinstance(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
pauli_list = operator[0]
|
|
456
|
-
coefficients = operator[1]
|
|
457
|
-
if (
|
|
458
|
-
coefficients is not None
|
|
459
|
-
and len(coefficients) > 0
|
|
460
|
-
and len(pauli_list) != len(coefficients)
|
|
461
|
-
):
|
|
590
|
+
if isinstance(coeffs, np.ndarray):
|
|
591
|
+
coeffs_seq = coeffs.ravel().tolist()
|
|
592
|
+
elif isinstance(coeffs, Sequence) and not isinstance(coeffs, (str, bytes)):
|
|
593
|
+
coeffs_seq = list(coeffs)
|
|
594
|
+
else:
|
|
462
595
|
raise Exception(
|
|
463
|
-
"
|
|
596
|
+
"Coefficients must be a numeric sequence (list/tuple/np.ndarray) or None."
|
|
464
597
|
)
|
|
465
598
|
|
|
599
|
+
if len(coeffs_seq) == 0:
|
|
600
|
+
return
|
|
601
|
+
if not all(_is_number(c) for c in coeffs_seq):
|
|
602
|
+
raise Exception("Operator coefficients must be numeric.")
|
|
603
|
+
if len(coeffs_seq) != len(pauli_list):
|
|
604
|
+
raise Exception(
|
|
605
|
+
"Number of coefficients must match number of Pauli terms (or empty/None for all-ones)."
|
|
606
|
+
)
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
raise Exception(
|
|
610
|
+
"Operator must be one of: Operator, Pauli, SparsePauliOp, PauliList, "
|
|
611
|
+
"or the tuple form (PauliList, coeffs_or_none)."
|
|
612
|
+
)
|
|
613
|
+
|
|
466
614
|
def to_sparse_pauli_operator(self, operator, coeffs=None):
|
|
467
615
|
if isinstance(operator, SparsePauliOp):
|
|
468
616
|
return operator
|
|
@@ -471,10 +619,14 @@ class InputData:
|
|
|
471
619
|
return SparsePauliOp(operator)
|
|
472
620
|
|
|
473
621
|
elif isinstance(operator, PauliList):
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
622
|
+
|
|
623
|
+
if coeffs is not None:
|
|
624
|
+
if not hasattr(coeffs, "__len__"):
|
|
625
|
+
raise ValueError("Coefficients must be a sequence (or None).")
|
|
626
|
+
if len(coeffs) > 0 and len(coeffs) != len(operator):
|
|
627
|
+
raise ValueError(
|
|
628
|
+
"Number of coefficients must match number of Pauli operators in PauliList"
|
|
629
|
+
)
|
|
478
630
|
|
|
479
631
|
coefficients = (
|
|
480
632
|
coeffs
|
|
@@ -486,14 +638,17 @@ class InputData:
|
|
|
486
638
|
|
|
487
639
|
elif isinstance(operator, Operator):
|
|
488
640
|
return SparsePauliOp.from_operator(operator)
|
|
641
|
+
raise ValueError(
|
|
642
|
+
"Unsupported operator type. Supported types are: Operator, Pauli, SparsePauliOp or PauliList and a possible empty list of numeric coefficents."
|
|
643
|
+
)
|
|
489
644
|
|
|
490
645
|
def serialize_sparse_pauli_operator(self, sparse_op):
|
|
491
646
|
if not isinstance(sparse_op, SparsePauliOp):
|
|
492
647
|
raise ValueError("Input must be a SparsePauliOp")
|
|
493
648
|
|
|
494
649
|
pauli_data = sparse_op.to_list()
|
|
495
|
-
pauli_terms = [term
|
|
496
|
-
coefficients = [
|
|
650
|
+
pauli_terms = [term for (term, _) in pauli_data]
|
|
651
|
+
coefficients = [_to_complex_dict(coeff) for (_, coeff) in pauli_data]
|
|
497
652
|
return pauli_terms, coefficients
|
|
498
653
|
|
|
499
654
|
|
|
@@ -508,8 +663,8 @@ class QuantumFlowsProvider:
|
|
|
508
663
|
_keycloak_url_dev = "http://localhost"
|
|
509
664
|
_keycloak_url_prod = "https://keycloak.transilvania-quantum.com"
|
|
510
665
|
|
|
511
|
-
def __init__(self,
|
|
512
|
-
self.
|
|
666
|
+
def __init__(self, verify_tls=True, debug=False):
|
|
667
|
+
self._verify_tls = verify_tls
|
|
513
668
|
self._debug = debug
|
|
514
669
|
self._state = None
|
|
515
670
|
self._access_token = None
|
|
@@ -539,7 +694,7 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
539
694
|
server_url=self._keycloak_server_url,
|
|
540
695
|
client_id=self._client_id,
|
|
541
696
|
realm_name=self._realm_name,
|
|
542
|
-
verify=self.
|
|
697
|
+
verify=self._verify_tls,
|
|
543
698
|
)
|
|
544
699
|
|
|
545
700
|
def authenticate(self):
|
|
@@ -573,26 +728,42 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
573
728
|
time.time() + token_response["refresh_expires_in"] - 5
|
|
574
729
|
) # seconds
|
|
575
730
|
print("Authentication successful.")
|
|
576
|
-
except AuthenticationFailure as ex:
|
|
577
|
-
print(ex.message)
|
|
578
731
|
except AuthorizationFailure as ex:
|
|
579
732
|
print(
|
|
580
733
|
"Failed to authenticate with the quantum provider. Make sure you are using the correct Gmail account."
|
|
581
734
|
)
|
|
582
735
|
if self._debug:
|
|
583
|
-
print("More details: ", ex
|
|
736
|
+
print("More details: ", str(ex))
|
|
737
|
+
traceback.print_exc()
|
|
738
|
+
except (KeyboardInterrupt, SystemExit):
|
|
739
|
+
raise
|
|
584
740
|
except Exception as ex:
|
|
585
741
|
print("Failed to authenticate with the quantum provider.")
|
|
586
742
|
if "Connection refused" in str(ex):
|
|
587
743
|
print("The remote service does not respond. Please try again later.")
|
|
588
744
|
if self._debug:
|
|
589
|
-
print("Unexpected exception: ", ex)
|
|
745
|
+
print("Unexpected exception: ", str(ex))
|
|
746
|
+
|
|
747
|
+
def _ensure_access_token(self) -> None:
|
|
748
|
+
# refresh token must exist and be valid
|
|
749
|
+
if self._refresh_token is None or self._refresh_token_expiration_time is None:
|
|
750
|
+
self._clear_tokens(clear_refresh=True)
|
|
751
|
+
raise AuthorizationFailure("Not authenticated; please authenticate.")
|
|
752
|
+
|
|
753
|
+
if self.is_refresh_token_expired():
|
|
754
|
+
self._clear_tokens(clear_refresh=True)
|
|
755
|
+
raise AuthorizationFailure("Session timed out; please re-authenticate.")
|
|
756
|
+
|
|
757
|
+
# access token must exist and be fresh; otherwise refresh
|
|
758
|
+
if self._access_token is None or self.is_token_expired():
|
|
759
|
+
ok = self._try_refresh_tokens()
|
|
760
|
+
if not ok or self._access_token is None:
|
|
761
|
+
# if refresh failed, _try_refresh_tokens already cleared appropriately
|
|
762
|
+
raise AuthorizationFailure("Session expired; please re-authenticate.")
|
|
590
763
|
|
|
591
764
|
def submit_job(
|
|
592
765
|
self, *, backend=None, circuit=None, circuits=None, shots=None, comments=""
|
|
593
766
|
):
|
|
594
|
-
if not self._verify_user_is_authenticated():
|
|
595
|
-
return
|
|
596
767
|
if not backend:
|
|
597
768
|
print("Please specify the backend name.")
|
|
598
769
|
return
|
|
@@ -639,7 +810,7 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
639
810
|
job_data = {
|
|
640
811
|
"BackendName": backend,
|
|
641
812
|
"Circuit": None,
|
|
642
|
-
"Circuits": [serialize_circuit(
|
|
813
|
+
"Circuits": [serialize_circuit(circ) for circ in circuits],
|
|
643
814
|
"Shots": shots,
|
|
644
815
|
"Comments": comments,
|
|
645
816
|
"QiskitVersion": qiskit.__version__,
|
|
@@ -648,12 +819,12 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
648
819
|
f"{self._asp_net_url}/api/job", job_data
|
|
649
820
|
)
|
|
650
821
|
if status_code == 201:
|
|
822
|
+
if not isinstance(result, dict):
|
|
823
|
+
raise Exception(
|
|
824
|
+
f"Expected JSON response for job creation, instead got:\n{str(result)}."
|
|
825
|
+
)
|
|
651
826
|
return Job(result["id"])
|
|
652
|
-
elif
|
|
653
|
-
print(
|
|
654
|
-
"You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
|
|
655
|
-
)
|
|
656
|
-
elif "Under Maintenance" in result:
|
|
827
|
+
elif isinstance(result, str) and "Under Maintenance" in result:
|
|
657
828
|
print(
|
|
658
829
|
"The remote service is currently under maintenance. Please try again later."
|
|
659
830
|
)
|
|
@@ -662,6 +833,12 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
662
833
|
f"Job submission has failed with http status code: {status_code}. \nRemote server response: '{result}'"
|
|
663
834
|
)
|
|
664
835
|
return Job(None)
|
|
836
|
+
except (KeyboardInterrupt, SystemExit):
|
|
837
|
+
raise
|
|
838
|
+
except AuthorizationFailure as ex:
|
|
839
|
+
print(str(ex))
|
|
840
|
+
except requests.exceptions.RequestException as ex:
|
|
841
|
+
print("Network error talking to provider:", ex)
|
|
665
842
|
except Exception as ex:
|
|
666
843
|
print(str(ex))
|
|
667
844
|
|
|
@@ -671,30 +848,50 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
671
848
|
backend=None,
|
|
672
849
|
shots=None,
|
|
673
850
|
workflow_id=None,
|
|
851
|
+
tag="",
|
|
674
852
|
comments="",
|
|
675
|
-
|
|
676
|
-
input_data=
|
|
853
|
+
max_fun_evaluations=None,
|
|
854
|
+
input_data=None,
|
|
677
855
|
):
|
|
678
|
-
if
|
|
679
|
-
|
|
856
|
+
if input_data is None:
|
|
857
|
+
input_data = InputData()
|
|
858
|
+
if shots is not None:
|
|
859
|
+
if not isinstance(shots, int) or shots <= 0:
|
|
860
|
+
print("The 'shots' input argument must be a positive integer.")
|
|
861
|
+
return
|
|
680
862
|
if not backend:
|
|
681
863
|
print("Please specify a backend name.")
|
|
682
864
|
return
|
|
865
|
+
if not isinstance(backend, str):
|
|
866
|
+
print("The 'backend' input argument must be a string.")
|
|
867
|
+
return
|
|
683
868
|
if not workflow_id:
|
|
684
869
|
print("Please specify a workflow Id.")
|
|
685
870
|
return
|
|
686
|
-
if shots is not None and not isinstance(shots, int):
|
|
687
|
-
print("The optional number of shots input argument must be an integer.")
|
|
688
|
-
return
|
|
689
871
|
if not self.is_valid_uuid(workflow_id):
|
|
690
872
|
print("The specified workflow Id is not a valid GUID.")
|
|
691
873
|
return
|
|
692
|
-
if
|
|
693
|
-
if not isinstance(
|
|
874
|
+
if max_fun_evaluations is not None:
|
|
875
|
+
if not isinstance(max_fun_evaluations, int) or max_fun_evaluations <= 0:
|
|
694
876
|
print(
|
|
695
|
-
"The optional 'max-
|
|
877
|
+
"The optional 'max-fun-evaluations' input argument must be a positive integer."
|
|
696
878
|
)
|
|
697
879
|
return
|
|
880
|
+
if not isinstance(tag, str):
|
|
881
|
+
print("The optional 'tag' input argument must be a string.")
|
|
882
|
+
return
|
|
883
|
+
if len(tag) > 100:
|
|
884
|
+
print("The optional 'tag' input argument must be at most 100 characters.")
|
|
885
|
+
return
|
|
886
|
+
if not isinstance(comments, str):
|
|
887
|
+
print("The optional 'comments' input argument must be a string.")
|
|
888
|
+
return
|
|
889
|
+
if len(comments) > 1000:
|
|
890
|
+
print(
|
|
891
|
+
"The optional 'comments' input argument must be at most 1000 characters."
|
|
892
|
+
)
|
|
893
|
+
return
|
|
894
|
+
|
|
698
895
|
try:
|
|
699
896
|
input_data_labels = []
|
|
700
897
|
input_data_items = []
|
|
@@ -702,20 +899,29 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
702
899
|
input_data_items.append(backend)
|
|
703
900
|
input_data_labels.append("shots")
|
|
704
901
|
input_data_items.append(str(shots))
|
|
705
|
-
input_data_labels.append("max-
|
|
706
|
-
input_data_items.append(str(
|
|
902
|
+
input_data_labels.append("max-fun-evaluations")
|
|
903
|
+
input_data_items.append(str(max_fun_evaluations))
|
|
707
904
|
for input_data_label in input_data.data.keys():
|
|
708
905
|
input_data_labels.append(input_data_label)
|
|
709
906
|
content = input_data.data[input_data_label]
|
|
710
|
-
|
|
711
|
-
json.dumps(content, indent=2, cls=CustomJSONEncoder)
|
|
712
|
-
|
|
907
|
+
try:
|
|
908
|
+
dumped = json.dumps(content, indent=2, cls=CustomJSONEncoder)
|
|
909
|
+
except ValueError as e:
|
|
910
|
+
raise Exception(
|
|
911
|
+
f"Input data '{input_data_label}' has invalid format: {e}"
|
|
912
|
+
)
|
|
913
|
+
except (OverflowError, TypeError) as e:
|
|
914
|
+
raise Exception(
|
|
915
|
+
f"Input data '{input_data_label}' content must be JSON serializable: {e}"
|
|
916
|
+
)
|
|
917
|
+
input_data_items.append(dumped)
|
|
713
918
|
job_data = {
|
|
714
919
|
"BackendName": backend,
|
|
715
920
|
"WorkflowId": workflow_id,
|
|
716
921
|
"Shots": shots,
|
|
922
|
+
"Tag": tag,
|
|
717
923
|
"Comments": comments,
|
|
718
|
-
"
|
|
924
|
+
"MaxFunEvaluations": max_fun_evaluations,
|
|
719
925
|
"InputDataLabels": input_data_labels,
|
|
720
926
|
"InputDataItems": input_data_items,
|
|
721
927
|
"QiskitVersion": qiskit.__version__,
|
|
@@ -724,22 +930,26 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
724
930
|
f"{self._asp_net_url}/api/workflow-job", job_data
|
|
725
931
|
)
|
|
726
932
|
if status_code == 201:
|
|
933
|
+
if not isinstance(result, dict):
|
|
934
|
+
raise Exception(
|
|
935
|
+
f"Expected JSON response for job creation, instead got:\n{str(result)}."
|
|
936
|
+
)
|
|
727
937
|
return WorkflowJob(result["id"])
|
|
728
|
-
elif status_code == 401:
|
|
729
|
-
print(
|
|
730
|
-
"You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
|
|
731
|
-
)
|
|
732
938
|
else:
|
|
733
939
|
print(
|
|
734
940
|
f"Workflow job submission has failed with http status code: {status_code}. \nRemote server response: '{result}'"
|
|
735
941
|
)
|
|
736
942
|
return WorkflowJob(None)
|
|
943
|
+
except (KeyboardInterrupt, SystemExit):
|
|
944
|
+
raise
|
|
945
|
+
except AuthorizationFailure as ex:
|
|
946
|
+
print(str(ex))
|
|
947
|
+
except requests.exceptions.RequestException as ex:
|
|
948
|
+
print("Network error talking to provider:", ex)
|
|
737
949
|
except Exception as ex:
|
|
738
950
|
print(str(ex))
|
|
739
951
|
|
|
740
952
|
def get_backends(self):
|
|
741
|
-
if not self._verify_user_is_authenticated():
|
|
742
|
-
return
|
|
743
953
|
try:
|
|
744
954
|
response = self._make_get_request(f"{self._asp_net_url}/api/backends")
|
|
745
955
|
status_code = response.status_code
|
|
@@ -755,16 +965,18 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
755
965
|
)
|
|
756
966
|
else:
|
|
757
967
|
print(f"Request has failed with http status code: {status_code}.")
|
|
968
|
+
except (KeyboardInterrupt, SystemExit):
|
|
969
|
+
raise
|
|
970
|
+
except AuthorizationFailure as ex:
|
|
971
|
+
print(str(ex))
|
|
758
972
|
except Exception as ex:
|
|
759
973
|
print(str(ex))
|
|
760
974
|
|
|
761
975
|
def get_job_status(self, job):
|
|
762
|
-
if
|
|
763
|
-
return
|
|
764
|
-
if job is None or job.id is None:
|
|
976
|
+
if job is None or job.id() is None:
|
|
765
977
|
print("This job is not valid.")
|
|
766
978
|
return
|
|
767
|
-
if
|
|
979
|
+
if isinstance(job, Job):
|
|
768
980
|
try:
|
|
769
981
|
response = self._make_get_request(
|
|
770
982
|
f"{self._asp_net_url}/api/job/status/{job.id()}"
|
|
@@ -774,9 +986,14 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
774
986
|
print("Job status: ", response.text)
|
|
775
987
|
else:
|
|
776
988
|
print(f"Request has failed with http status code: {status_code}.")
|
|
989
|
+
except (KeyboardInterrupt, SystemExit):
|
|
990
|
+
raise
|
|
991
|
+
except AuthorizationFailure as ex:
|
|
992
|
+
print(str(ex))
|
|
993
|
+
return
|
|
777
994
|
except Exception as ex:
|
|
778
995
|
print(str(ex))
|
|
779
|
-
elif
|
|
996
|
+
elif isinstance(job, WorkflowJob):
|
|
780
997
|
try:
|
|
781
998
|
response = self._make_get_request(
|
|
782
999
|
f"{self._asp_net_url}/api/workflow-job/status/{job.id()}"
|
|
@@ -786,16 +1003,18 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
786
1003
|
print("Job status: ", response.text)
|
|
787
1004
|
else:
|
|
788
1005
|
print(f"Request has failed with http status code: {status_code}.")
|
|
1006
|
+
except (KeyboardInterrupt, SystemExit):
|
|
1007
|
+
raise
|
|
1008
|
+
except AuthorizationFailure as ex:
|
|
1009
|
+
print(str(ex))
|
|
789
1010
|
except Exception as ex:
|
|
790
1011
|
print(str(ex))
|
|
791
1012
|
|
|
792
1013
|
def get_job_result(self, job):
|
|
793
|
-
if
|
|
794
|
-
return
|
|
795
|
-
if job is None or job.id is None:
|
|
1014
|
+
if job is None or job.id() is None:
|
|
796
1015
|
print("This job is not valid.")
|
|
797
1016
|
return
|
|
798
|
-
if
|
|
1017
|
+
if isinstance(job, Job):
|
|
799
1018
|
try:
|
|
800
1019
|
response = self._make_get_request(
|
|
801
1020
|
f"{self._asp_net_url}/api/job/result/{job.id()}"
|
|
@@ -805,48 +1024,73 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
805
1024
|
print(response.text)
|
|
806
1025
|
else:
|
|
807
1026
|
print(f"Request has failed with http status code: {status_code}.")
|
|
1027
|
+
except (KeyboardInterrupt, SystemExit):
|
|
1028
|
+
raise
|
|
1029
|
+
except AuthorizationFailure as ex:
|
|
1030
|
+
print(str(ex))
|
|
808
1031
|
except Exception as ex:
|
|
809
1032
|
print(str(ex))
|
|
810
|
-
elif
|
|
1033
|
+
elif isinstance(job, WorkflowJob):
|
|
811
1034
|
print("Operation not supported for workflow jobs.")
|
|
812
1035
|
|
|
813
|
-
def _verify_user_is_authenticated(self):
|
|
814
|
-
if (
|
|
815
|
-
self._access_token is None
|
|
816
|
-
or self._refresh_token is None
|
|
817
|
-
or self._refresh_token_expiration_time is None
|
|
818
|
-
):
|
|
819
|
-
print(
|
|
820
|
-
"You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
|
|
821
|
-
)
|
|
822
|
-
return False
|
|
823
|
-
if self.is_refresh_token_expired():
|
|
824
|
-
print("You session timed out, you need to re-authenticate!")
|
|
825
|
-
return False
|
|
826
|
-
return True
|
|
827
|
-
|
|
828
1036
|
def _make_get_request(self, api_url):
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return requests.get(
|
|
1037
|
+
self._ensure_access_token()
|
|
1038
|
+
response = requests.get(
|
|
832
1039
|
api_url,
|
|
833
1040
|
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
834
|
-
verify=self.
|
|
1041
|
+
verify=self._verify_tls,
|
|
1042
|
+
timeout=DEFAULT_TIMEOUT,
|
|
835
1043
|
)
|
|
1044
|
+
if response.status_code == 401:
|
|
1045
|
+
# access token rejected; try refresh once
|
|
1046
|
+
self._access_token = None
|
|
1047
|
+
self._token_expiration_time = None
|
|
1048
|
+
self._ensure_access_token()
|
|
1049
|
+
response = requests.get(
|
|
1050
|
+
api_url,
|
|
1051
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
1052
|
+
verify=self._verify_tls,
|
|
1053
|
+
timeout=DEFAULT_TIMEOUT,
|
|
1054
|
+
)
|
|
1055
|
+
# second 401 => hard fail
|
|
1056
|
+
if response.status_code == 401:
|
|
1057
|
+
self._clear_tokens(clear_refresh=True)
|
|
1058
|
+
raise AuthorizationFailure(
|
|
1059
|
+
"You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
|
|
1060
|
+
)
|
|
1061
|
+
return response
|
|
836
1062
|
|
|
837
1063
|
def _make_post_request(self, api_url, data):
|
|
838
|
-
|
|
839
|
-
self._try_refresh_tokens()
|
|
1064
|
+
self._ensure_access_token()
|
|
840
1065
|
response = requests.post(
|
|
841
1066
|
api_url,
|
|
842
1067
|
json=data,
|
|
843
1068
|
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
844
|
-
verify=self.
|
|
1069
|
+
verify=self._verify_tls,
|
|
1070
|
+
timeout=DEFAULT_TIMEOUT,
|
|
845
1071
|
)
|
|
1072
|
+
if response.status_code == 401:
|
|
1073
|
+
# access token rejected; try refresh once
|
|
1074
|
+
self._access_token = None
|
|
1075
|
+
self._token_expiration_time = None
|
|
1076
|
+
self._ensure_access_token()
|
|
1077
|
+
response = requests.post(
|
|
1078
|
+
api_url,
|
|
1079
|
+
json=data,
|
|
1080
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
1081
|
+
verify=self._verify_tls,
|
|
1082
|
+
timeout=DEFAULT_TIMEOUT,
|
|
1083
|
+
)
|
|
1084
|
+
# second 401 => hard fail
|
|
1085
|
+
if response.status_code == 401:
|
|
1086
|
+
self._clear_tokens(clear_refresh=True)
|
|
1087
|
+
raise AuthorizationFailure(
|
|
1088
|
+
"You are not authorized to access this service. Please try to authenticate first and make sure you have signed on on our web-site with a Google email account."
|
|
1089
|
+
)
|
|
846
1090
|
try:
|
|
847
|
-
|
|
848
|
-
return (response.status_code,
|
|
849
|
-
except:
|
|
1091
|
+
payload = response.json()
|
|
1092
|
+
return (response.status_code, payload)
|
|
1093
|
+
except ValueError:
|
|
850
1094
|
return (response.status_code, response.text)
|
|
851
1095
|
|
|
852
1096
|
def is_token_expired(self):
|
|
@@ -859,34 +1103,80 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
859
1103
|
return True
|
|
860
1104
|
return time.time() > self._refresh_token_expiration_time
|
|
861
1105
|
|
|
862
|
-
def
|
|
1106
|
+
def _clear_tokens(self, *, clear_refresh: bool = True) -> None:
|
|
1107
|
+
self._access_token = None
|
|
1108
|
+
self._token_expiration_time = None
|
|
1109
|
+
if clear_refresh:
|
|
1110
|
+
self._refresh_token = None
|
|
1111
|
+
self._refresh_token_expiration_time = None
|
|
1112
|
+
|
|
1113
|
+
def _try_refresh_tokens(self) -> bool:
|
|
863
1114
|
try:
|
|
864
1115
|
token_response = self._keycloak_openid.token(
|
|
865
|
-
grant_type="refresh_token",
|
|
1116
|
+
grant_type="refresh_token",
|
|
1117
|
+
refresh_token=self._refresh_token,
|
|
866
1118
|
)
|
|
867
1119
|
self._access_token = token_response["access_token"]
|
|
868
1120
|
self._refresh_token = token_response["refresh_token"]
|
|
869
|
-
self._token_expiration_time = (
|
|
870
|
-
time.time() + token_response["expires_in"] - 5
|
|
871
|
-
) # seconds
|
|
1121
|
+
self._token_expiration_time = time.time() + token_response["expires_in"] - 5
|
|
872
1122
|
self._refresh_token_expiration_time = (
|
|
873
1123
|
time.time() + token_response["refresh_expires_in"] - 5
|
|
874
|
-
)
|
|
875
|
-
|
|
876
|
-
|
|
1124
|
+
)
|
|
1125
|
+
return True
|
|
1126
|
+
except (KeyboardInterrupt, SystemExit):
|
|
1127
|
+
raise
|
|
1128
|
+
except Exception as ex:
|
|
1129
|
+
clear_refresh = False
|
|
1130
|
+
is_transient = isinstance(ex, requests.exceptions.RequestException)
|
|
1131
|
+
msg = str(ex).lower()
|
|
1132
|
+
transient_markers = [
|
|
1133
|
+
"timeout",
|
|
1134
|
+
"timed out",
|
|
1135
|
+
"connection aborted",
|
|
1136
|
+
"connection refused",
|
|
1137
|
+
"connection reset",
|
|
1138
|
+
"temporary failure",
|
|
1139
|
+
"temporarily unavailable",
|
|
1140
|
+
"service unavailable",
|
|
1141
|
+
"bad gateway",
|
|
1142
|
+
"gateway timeout",
|
|
1143
|
+
"name or service not known",
|
|
1144
|
+
"dns",
|
|
1145
|
+
]
|
|
1146
|
+
if any(m in msg for m in transient_markers):
|
|
1147
|
+
is_transient = True
|
|
1148
|
+
invalid_markers = [
|
|
1149
|
+
"invalid_grant",
|
|
1150
|
+
"refresh token is invalid",
|
|
1151
|
+
"refresh_token is invalid",
|
|
1152
|
+
"token is not active",
|
|
1153
|
+
"session not active",
|
|
1154
|
+
]
|
|
1155
|
+
if (not is_transient) and any(m in msg for m in invalid_markers):
|
|
1156
|
+
clear_refresh = True
|
|
1157
|
+
self._clear_tokens(clear_refresh=clear_refresh)
|
|
1158
|
+
if self._debug:
|
|
1159
|
+
print("Failed to refresh authentication tokens:", ex)
|
|
1160
|
+
print(
|
|
1161
|
+
"Classified as transient:",
|
|
1162
|
+
is_transient,
|
|
1163
|
+
"clear_refresh:",
|
|
1164
|
+
clear_refresh,
|
|
1165
|
+
)
|
|
1166
|
+
return False
|
|
877
1167
|
|
|
878
1168
|
def _is_server_online(self, url):
|
|
879
1169
|
try:
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
return True
|
|
883
|
-
return False
|
|
1170
|
+
requests.get(url, verify=self._verify_tls, timeout=DEFAULT_TIMEOUT)
|
|
1171
|
+
return True
|
|
884
1172
|
except requests.exceptions.RequestException as e:
|
|
885
1173
|
return False
|
|
886
1174
|
|
|
887
1175
|
def _is_server_under_maintenance(self, url):
|
|
888
1176
|
try:
|
|
889
|
-
response = requests.get(
|
|
1177
|
+
response = requests.get(
|
|
1178
|
+
url, verify=self._verify_tls, timeout=DEFAULT_TIMEOUT
|
|
1179
|
+
)
|
|
890
1180
|
if "Under Maintenance" in response.text:
|
|
891
1181
|
return True
|
|
892
1182
|
return False
|
|
@@ -898,7 +1188,8 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
898
1188
|
response = requests.post(
|
|
899
1189
|
f"{self._asp_net_url}/auth/storestate",
|
|
900
1190
|
json={"state": state},
|
|
901
|
-
verify=self.
|
|
1191
|
+
verify=self._verify_tls,
|
|
1192
|
+
timeout=DEFAULT_TIMEOUT,
|
|
902
1193
|
)
|
|
903
1194
|
if response.status_code != 200:
|
|
904
1195
|
if not self._is_server_online(self._asp_net_url):
|
|
@@ -930,22 +1221,34 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
930
1221
|
|
|
931
1222
|
timeout_seconds = 16
|
|
932
1223
|
start_time = time.time()
|
|
1224
|
+
accept_missing_code_once = True
|
|
933
1225
|
|
|
934
1226
|
try:
|
|
935
1227
|
while (time.time() - start_time) < timeout_seconds:
|
|
936
1228
|
response = requests.get(
|
|
937
1229
|
self._show_code_callback_url,
|
|
938
1230
|
params={"state": self._state},
|
|
939
|
-
verify=self.
|
|
1231
|
+
verify=self._verify_tls,
|
|
1232
|
+
timeout=DEFAULT_TIMEOUT,
|
|
940
1233
|
)
|
|
941
|
-
|
|
942
|
-
if response.status_code == 400:
|
|
1234
|
+
if response.status_code != 200:
|
|
943
1235
|
if response.text == "Authorization state is missing.":
|
|
944
|
-
raise Exception(
|
|
1236
|
+
raise Exception(
|
|
1237
|
+
"The authentication process was not initiated properly. Please try to authenticate again."
|
|
1238
|
+
)
|
|
945
1239
|
time.sleep(1)
|
|
946
1240
|
continue
|
|
947
|
-
|
|
948
|
-
|
|
1241
|
+
parts = response.text.split("Your authorization code is: ", 1)
|
|
1242
|
+
if len(parts) != 2 or not parts[1].strip():
|
|
1243
|
+
if accept_missing_code_once:
|
|
1244
|
+
accept_missing_code_once = False
|
|
1245
|
+
time.sleep(3)
|
|
1246
|
+
continue
|
|
1247
|
+
else:
|
|
1248
|
+
raise Exception(
|
|
1249
|
+
"The server failed to provide a valid authorization code. Please try to authenticate again, if it keeps failing send a bug report using the feedback form."
|
|
1250
|
+
)
|
|
1251
|
+
auth_code = parts[1].strip()
|
|
949
1252
|
return auth_code
|
|
950
1253
|
except Exception as e:
|
|
951
1254
|
if self._debug:
|
|
@@ -958,9 +1261,11 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
958
1261
|
"Authorization code was not received. Please make sure you are using a Google account which you have signed-on our web-site. If our website is not online please try again later."
|
|
959
1262
|
)
|
|
960
1263
|
|
|
961
|
-
def is_valid_uuid(self, value
|
|
1264
|
+
def is_valid_uuid(self, value) -> bool:
|
|
1265
|
+
if not isinstance(value, str):
|
|
1266
|
+
return False
|
|
962
1267
|
try:
|
|
963
|
-
|
|
964
|
-
return
|
|
965
|
-
except (ValueError, TypeError):
|
|
1268
|
+
uuid.UUID(value) # parses many valid forms
|
|
1269
|
+
return True
|
|
1270
|
+
except (ValueError, TypeError, AttributeError):
|
|
966
1271
|
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|