quantum-flows 0.1.14__py3-none-any.whl → 0.1.15__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.
- quantum_flows/quantum_flows.py +8 -8
- quantum_flows/quantum_flows.py.bck +966 -0
- {quantum_flows-0.1.14.dist-info → quantum_flows-0.1.15.dist-info}/METADATA +1 -1
- quantum_flows-0.1.15.dist-info/RECORD +7 -0
- quantum_flows-0.1.14.dist-info/RECORD +0 -6
- {quantum_flows-0.1.14.dist-info → quantum_flows-0.1.15.dist-info}/LICENSE +0 -0
- {quantum_flows-0.1.14.dist-info → quantum_flows-0.1.15.dist-info}/WHEEL +0 -0
quantum_flows/quantum_flows.py
CHANGED
|
@@ -148,7 +148,7 @@ class InputData:
|
|
|
148
148
|
"ising-model",
|
|
149
149
|
"lattice",
|
|
150
150
|
"lp-model",
|
|
151
|
-
"max-
|
|
151
|
+
"max-fun-evaluations",
|
|
152
152
|
"molecule-info",
|
|
153
153
|
"operator",
|
|
154
154
|
"pub",
|
|
@@ -672,7 +672,7 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
672
672
|
shots=None,
|
|
673
673
|
workflow_id=None,
|
|
674
674
|
comments="",
|
|
675
|
-
|
|
675
|
+
max_fun_evaluations=None,
|
|
676
676
|
input_data=InputData(),
|
|
677
677
|
):
|
|
678
678
|
if not self._verify_user_is_authenticated():
|
|
@@ -689,10 +689,10 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
689
689
|
if not self.is_valid_uuid(workflow_id):
|
|
690
690
|
print("The specified workflow Id is not a valid GUID.")
|
|
691
691
|
return
|
|
692
|
-
if
|
|
693
|
-
if not isinstance(
|
|
692
|
+
if max_fun_evaluations is not None:
|
|
693
|
+
if not isinstance(max_fun_evaluations, int) or max_fun_evaluations <= 0:
|
|
694
694
|
print(
|
|
695
|
-
"The optional 'max-
|
|
695
|
+
"The optional 'max-fun-evaluations' input argument must be a positive integer."
|
|
696
696
|
)
|
|
697
697
|
return
|
|
698
698
|
try:
|
|
@@ -702,8 +702,8 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
702
702
|
input_data_items.append(backend)
|
|
703
703
|
input_data_labels.append("shots")
|
|
704
704
|
input_data_items.append(str(shots))
|
|
705
|
-
input_data_labels.append("max-
|
|
706
|
-
input_data_items.append(str(
|
|
705
|
+
input_data_labels.append("max-fun-evaluations")
|
|
706
|
+
input_data_items.append(str(max_fun_evaluations))
|
|
707
707
|
for input_data_label in input_data.data.keys():
|
|
708
708
|
input_data_labels.append(input_data_label)
|
|
709
709
|
content = input_data.data[input_data_label]
|
|
@@ -715,7 +715,7 @@ In case the service has been recently started please wait 5 minutes for it to be
|
|
|
715
715
|
"WorkflowId": workflow_id,
|
|
716
716
|
"Shots": shots,
|
|
717
717
|
"Comments": comments,
|
|
718
|
-
"MaxIterations":
|
|
718
|
+
"MaxIterations": max_fun_evaluations,
|
|
719
719
|
"InputDataLabels": input_data_labels,
|
|
720
720
|
"InputDataItems": input_data_items,
|
|
721
721
|
"QiskitVersion": qiskit.__version__,
|
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import numpy as np
|
|
5
|
+
import qiskit
|
|
6
|
+
import requests
|
|
7
|
+
import secrets
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
import webbrowser
|
|
11
|
+
|
|
12
|
+
from IPython.display import display, HTML
|
|
13
|
+
from keycloak import KeycloakOpenID
|
|
14
|
+
from urllib.parse import urlencode
|
|
15
|
+
|
|
16
|
+
from qiskit import qpy
|
|
17
|
+
from qiskit import QuantumCircuit
|
|
18
|
+
from qiskit.quantum_info import Operator, Pauli, PauliList, SparsePauliOp
|
|
19
|
+
from qiskit.quantum_info.operators.linear_op import LinearOp
|
|
20
|
+
from qiskit_nature.second_q.hamiltonians.lattices import (
|
|
21
|
+
KagomeLattice,
|
|
22
|
+
Lattice,
|
|
23
|
+
LineLattice,
|
|
24
|
+
HexagonalLattice,
|
|
25
|
+
HyperCubicLattice,
|
|
26
|
+
SquareLattice,
|
|
27
|
+
TriangularLattice,
|
|
28
|
+
)
|
|
29
|
+
from qiskit_nature.second_q.hamiltonians.lattices.boundary_condition import (
|
|
30
|
+
BoundaryCondition,
|
|
31
|
+
)
|
|
32
|
+
from qiskit_optimization import QuadraticProgram
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CustomJSONEncoder(json.JSONEncoder):
|
|
36
|
+
def default(self, obj):
|
|
37
|
+
if isinstance(obj, complex):
|
|
38
|
+
return {"real": obj.real, "imag": obj.imag}
|
|
39
|
+
if isinstance(obj, np.ndarray):
|
|
40
|
+
return obj.tolist()
|
|
41
|
+
if isinstance(obj, BoundaryCondition):
|
|
42
|
+
# Convert enum to string
|
|
43
|
+
return str(obj)
|
|
44
|
+
return super().default(obj)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def all_numbers(lst):
|
|
48
|
+
return all(isinstance(x, (int, float, complex)) for x in lst)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def serialize_circuit(circuit):
|
|
52
|
+
buffer = io.BytesIO()
|
|
53
|
+
qpy.dump(circuit, buffer)
|
|
54
|
+
qpy_binary_data = buffer.getvalue()
|
|
55
|
+
base64_encoded_circuit = base64.b64encode(qpy_binary_data).decode("utf-8")
|
|
56
|
+
return base64_encoded_circuit
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AuthenticationFailure(Exception):
|
|
60
|
+
def __init__(self, message):
|
|
61
|
+
self.message = message
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AuthorizationFailure(Exception):
|
|
65
|
+
def __init__(self, message):
|
|
66
|
+
self.message = message
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Job:
|
|
70
|
+
def __init__(self, job_id):
|
|
71
|
+
self._job_id = job_id
|
|
72
|
+
|
|
73
|
+
def id(self):
|
|
74
|
+
return self._job_id
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class WorkflowJob:
|
|
78
|
+
def __init__(self, job_id):
|
|
79
|
+
self._job_id = job_id
|
|
80
|
+
|
|
81
|
+
def id(self):
|
|
82
|
+
return self._job_id
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class InputData:
|
|
86
|
+
def __init__(self, label=None, content=None):
|
|
87
|
+
self.data = {}
|
|
88
|
+
if label:
|
|
89
|
+
self.add_data(label, content)
|
|
90
|
+
|
|
91
|
+
def __str__(self):
|
|
92
|
+
return json.dumps(self.data, indent=2, cls=CustomJSONEncoder)
|
|
93
|
+
|
|
94
|
+
def add_data(self, label, content):
|
|
95
|
+
self.check_label(label, self.data)
|
|
96
|
+
try:
|
|
97
|
+
if label == "operator":
|
|
98
|
+
operator = content
|
|
99
|
+
self.validate_operator(operator)
|
|
100
|
+
coeffs = None
|
|
101
|
+
if type(content) == tuple:
|
|
102
|
+
operator, coeffs = content
|
|
103
|
+
sparse_pauli_operator = self.to_sparse_pauli_operator(
|
|
104
|
+
operator, coeffs=coeffs
|
|
105
|
+
)
|
|
106
|
+
pauli_terms, coefficients = self.serialize_sparse_pauli_operator(
|
|
107
|
+
sparse_pauli_operator
|
|
108
|
+
)
|
|
109
|
+
self.data["operator"] = {
|
|
110
|
+
"pauli-terms": pauli_terms,
|
|
111
|
+
"coefficients": coefficients,
|
|
112
|
+
"operator-string-representation": str(operator),
|
|
113
|
+
}
|
|
114
|
+
elif label == "pub":
|
|
115
|
+
content = self.validate_and_serialize_pub(content)
|
|
116
|
+
if not "pubs" in self.data.keys():
|
|
117
|
+
self.data["pubs"] = []
|
|
118
|
+
self.data["pubs"].append(content)
|
|
119
|
+
elif label == "molecule-info":
|
|
120
|
+
self.validate_molecule_info(content)
|
|
121
|
+
self.data[label] = content
|
|
122
|
+
elif label == "lattice":
|
|
123
|
+
self.data[label] = self.lattice_to_dict(content)
|
|
124
|
+
elif label == "ising-model":
|
|
125
|
+
self.validate_ising_model(content)
|
|
126
|
+
self.data[label] = content
|
|
127
|
+
elif label == "training-data":
|
|
128
|
+
self.validate_training_data(content)
|
|
129
|
+
self.data[label] = content
|
|
130
|
+
elif label == "inference-data":
|
|
131
|
+
self.validate_inference_data(content)
|
|
132
|
+
self.data[label] = content
|
|
133
|
+
elif label == "quadratic-program":
|
|
134
|
+
self.validate_quadratic_program(content)
|
|
135
|
+
lp_string = content.export_as_lp_string()
|
|
136
|
+
self.data[label] = lp_string
|
|
137
|
+
else:
|
|
138
|
+
self.data[label] = content
|
|
139
|
+
except (OverflowError, TypeError, ValueError):
|
|
140
|
+
raise Exception("Input data content must be JSON serializable.")
|
|
141
|
+
|
|
142
|
+
def check_label(self, label, data):
|
|
143
|
+
if type(label) != str:
|
|
144
|
+
raise Exception("Input data label must be string.")
|
|
145
|
+
if label not in [
|
|
146
|
+
"ansatz-parameters",
|
|
147
|
+
"inference-data",
|
|
148
|
+
"ising-model",
|
|
149
|
+
"lattice",
|
|
150
|
+
"lp-model",
|
|
151
|
+
"max-iterations",
|
|
152
|
+
"molecule-info",
|
|
153
|
+
"operator",
|
|
154
|
+
"pub",
|
|
155
|
+
"quadratic-program",
|
|
156
|
+
"training-data",
|
|
157
|
+
]:
|
|
158
|
+
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'."
|
|
160
|
+
)
|
|
161
|
+
if label != "pub" and label in data.keys():
|
|
162
|
+
raise Exception(
|
|
163
|
+
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
|
+
)
|
|
165
|
+
|
|
166
|
+
def lattice_to_dict(self, lattice):
|
|
167
|
+
if isinstance(lattice, LineLattice):
|
|
168
|
+
lattice_data = {
|
|
169
|
+
"type": "LineLattice",
|
|
170
|
+
"num_nodes": lattice.num_nodes,
|
|
171
|
+
"boundary_condition": lattice.boundary_condition[0].name,
|
|
172
|
+
"edge_parameter": lattice.edge_parameter,
|
|
173
|
+
"onsite_parameter": lattice.onsite_parameter,
|
|
174
|
+
}
|
|
175
|
+
return lattice_data
|
|
176
|
+
elif isinstance(lattice, TriangularLattice):
|
|
177
|
+
lattice_data = {
|
|
178
|
+
"type": "TriangularLattice",
|
|
179
|
+
"rows": lattice.rows,
|
|
180
|
+
"cols": lattice.cols,
|
|
181
|
+
"boundary_condition": lattice.boundary_condition.name,
|
|
182
|
+
"edge_parameter": lattice.edge_parameter,
|
|
183
|
+
"onsite_parameter": lattice.onsite_parameter,
|
|
184
|
+
}
|
|
185
|
+
return lattice_data
|
|
186
|
+
elif isinstance(lattice, (SquareLattice, KagomeLattice, HyperCubicLattice)):
|
|
187
|
+
lattice_data = {
|
|
188
|
+
"type": type(lattice).__name__,
|
|
189
|
+
"rows": lattice.rows,
|
|
190
|
+
"cols": lattice.cols,
|
|
191
|
+
"boundary_condition": [
|
|
192
|
+
bc.name
|
|
193
|
+
for bc in (
|
|
194
|
+
lattice.boundary_condition
|
|
195
|
+
if isinstance(lattice.boundary_condition, tuple)
|
|
196
|
+
else (lattice.boundary_condition,)
|
|
197
|
+
)
|
|
198
|
+
],
|
|
199
|
+
"edge_parameter": lattice.edge_parameter,
|
|
200
|
+
"onsite_parameter": lattice.onsite_parameter,
|
|
201
|
+
}
|
|
202
|
+
return lattice_data
|
|
203
|
+
elif isinstance(lattice, HexagonalLattice):
|
|
204
|
+
lattice_data = {
|
|
205
|
+
"type": "HexagonalLattice",
|
|
206
|
+
"rows": lattice._rows,
|
|
207
|
+
"cols": lattice._cols,
|
|
208
|
+
"edge_parameter": lattice.edge_parameter,
|
|
209
|
+
"onsite_parameter": lattice.onsite_parameter,
|
|
210
|
+
}
|
|
211
|
+
return lattice_data
|
|
212
|
+
elif isinstance(lattice, Lattice):
|
|
213
|
+
graph = lattice.graph
|
|
214
|
+
nodes = list(graph.node_indexes())
|
|
215
|
+
edges = [
|
|
216
|
+
{"source": edge[0], "target": edge[1], "weight": edge[2]}
|
|
217
|
+
for edge in graph.weighted_edge_list()
|
|
218
|
+
]
|
|
219
|
+
lattice_data = {
|
|
220
|
+
"type": "Lattice",
|
|
221
|
+
"nodes": nodes,
|
|
222
|
+
"edges": edges,
|
|
223
|
+
"num_nodes": lattice.num_nodes,
|
|
224
|
+
}
|
|
225
|
+
return lattice_data
|
|
226
|
+
else:
|
|
227
|
+
raise Exception(
|
|
228
|
+
"This input lattice object is not supported. Please use an object of the following types: Lattice, LineLattice, TriangularLattice, SquareLattice, KagomeLattice, HyperCubicLattice or HexagonalLattice. All of them are available in the qiskit_nature library."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def validate_molecule_info(self, molecule_info):
|
|
232
|
+
if not isinstance(molecule_info["symbols"], list):
|
|
233
|
+
raise Exception("The 'symbols' must be a list of nuclei.")
|
|
234
|
+
if not isinstance(molecule_info["coords"], list):
|
|
235
|
+
raise Exception(
|
|
236
|
+
"The 'coords' must be a list of tuples representing the x, y, z position of each nuclei."
|
|
237
|
+
)
|
|
238
|
+
if "mutiplicity" in molecule_info and not isinstance(
|
|
239
|
+
molecule_info["mutiplicity"], int
|
|
240
|
+
):
|
|
241
|
+
raise Exception("The 'multiplicity' must be an integer.")
|
|
242
|
+
if "charge" in molecule_info and not isinstance(molecule_info["charge"], int):
|
|
243
|
+
raise Exception("The 'charge' must be an integer.")
|
|
244
|
+
if (
|
|
245
|
+
"units" in molecule_info
|
|
246
|
+
and molecule_info["units"].lower() != "angstrom"
|
|
247
|
+
and molecule_info["units"].lower() != "bohr"
|
|
248
|
+
):
|
|
249
|
+
raise Exception("The 'units' must be either 'Angstrom' or 'Bohr'.")
|
|
250
|
+
if "masses" in molecule_info and not all(
|
|
251
|
+
isinstance(m, (int, float)) for m in molecule_info["masses"]
|
|
252
|
+
):
|
|
253
|
+
raise Exception(
|
|
254
|
+
"The 'masses' must be a list of floats, one for each nucleus in the molecule."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def validate_ising_model(self, ising_model):
|
|
258
|
+
if not isinstance(ising_model, dict):
|
|
259
|
+
raise Exception("The 'ising_model' must be a dictionary.")
|
|
260
|
+
|
|
261
|
+
for key in ising_model.keys():
|
|
262
|
+
if key not in ["h", "J"]:
|
|
263
|
+
raise Exception(
|
|
264
|
+
"The 'ising_model' dictionary can only contain the keys: 'h' and 'J'."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if "h" in ising_model:
|
|
268
|
+
if not isinstance(ising_model["h"], list):
|
|
269
|
+
raise Exception("The 'h' field must be a list of numeric values.")
|
|
270
|
+
if not all(isinstance(h, (int, float)) for h in ising_model["h"]):
|
|
271
|
+
raise Exception("Each element in 'h' must be an int or float.")
|
|
272
|
+
|
|
273
|
+
if "J" in ising_model:
|
|
274
|
+
if not isinstance(ising_model["J"], list):
|
|
275
|
+
raise Exception("The 'J' field must be a list of dictionaries.")
|
|
276
|
+
for interaction in ising_model["J"]:
|
|
277
|
+
if not isinstance(interaction, dict):
|
|
278
|
+
raise Exception(
|
|
279
|
+
"Each item in 'J' must be a dictionary with 'pair' and 'value' keys."
|
|
280
|
+
)
|
|
281
|
+
if "pair" not in interaction or "value" not in interaction:
|
|
282
|
+
raise Exception(
|
|
283
|
+
"Each item in 'J' must contain 'pair' and 'value' keys."
|
|
284
|
+
)
|
|
285
|
+
if (
|
|
286
|
+
not isinstance(interaction["pair"], list)
|
|
287
|
+
or len(interaction["pair"]) != 2
|
|
288
|
+
or not all(isinstance(i, int) for i in interaction["pair"])
|
|
289
|
+
):
|
|
290
|
+
raise Exception("'pair' must be a list of two integers.")
|
|
291
|
+
if not isinstance(interaction["value"], (int, float)):
|
|
292
|
+
raise Exception("'value' must be a numeric type (int or float).")
|
|
293
|
+
|
|
294
|
+
def validate_training_data(self, training_data):
|
|
295
|
+
vector_size = None
|
|
296
|
+
if not isinstance(training_data, list):
|
|
297
|
+
raise Exception("The 'training_data' must be a list of dictionaries.")
|
|
298
|
+
for data in training_data:
|
|
299
|
+
if not isinstance(data, dict):
|
|
300
|
+
raise Exception("The 'training_data' must be a list of dictionaries.")
|
|
301
|
+
if not "data-point" in data:
|
|
302
|
+
raise Exception(
|
|
303
|
+
"Each dictionary in the list 'training_data' must contain a 'data-point' key."
|
|
304
|
+
)
|
|
305
|
+
vector = data["data-point"]
|
|
306
|
+
data_tags = data["data-tags"] if "data-tags" in data else None
|
|
307
|
+
if not isinstance(vector, list):
|
|
308
|
+
raise Exception(
|
|
309
|
+
"The 'data-point' value must be a list of numeric values."
|
|
310
|
+
)
|
|
311
|
+
if data_tags is not None and not isinstance(data_tags, list):
|
|
312
|
+
raise Exception(
|
|
313
|
+
"The optional 'data-tags' value must be a list of strings."
|
|
314
|
+
)
|
|
315
|
+
if not all(isinstance(item, (int, float)) for item in vector):
|
|
316
|
+
raise Exception(
|
|
317
|
+
"The 'data-point' value must be a list of numeric values (int or float)."
|
|
318
|
+
)
|
|
319
|
+
if vector_size is None:
|
|
320
|
+
vector_size = len(vector)
|
|
321
|
+
if len(vector) != vector_size:
|
|
322
|
+
raise Exception(
|
|
323
|
+
"All 'data-point' vectors in training data entries must have the same length."
|
|
324
|
+
)
|
|
325
|
+
if data_tags is not None and len(data_tags) != vector_size:
|
|
326
|
+
raise Exception(
|
|
327
|
+
"If provided, the 'data-tags' list must have the same length as the 'data-point' vector."
|
|
328
|
+
)
|
|
329
|
+
if not "label" in data:
|
|
330
|
+
raise Exception(
|
|
331
|
+
"Each dictionary in the list of training data points must contain a 'label' key."
|
|
332
|
+
)
|
|
333
|
+
label = data["label"]
|
|
334
|
+
if not isinstance(label, (int, float)):
|
|
335
|
+
raise Exception(
|
|
336
|
+
"The 'label' value must be a numeric type (int or float)."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def validate_inference_data(self, inference_data):
|
|
340
|
+
vector_size = None
|
|
341
|
+
if not isinstance(inference_data, list):
|
|
342
|
+
raise Exception("The 'inference_data' must be a list of dictionaries.")
|
|
343
|
+
for data in inference_data:
|
|
344
|
+
if not isinstance(data, dict):
|
|
345
|
+
raise Exception("The 'inference_data' must be a list of dictionaries.")
|
|
346
|
+
if not "data-point" in data:
|
|
347
|
+
raise Exception(
|
|
348
|
+
"Each dictionary in the list of inference data points must contain a 'data-point' key."
|
|
349
|
+
)
|
|
350
|
+
vector = data["data-point"]
|
|
351
|
+
data_tags = data["data-tags"] if "data-tags" in data else None
|
|
352
|
+
if not isinstance(vector, list):
|
|
353
|
+
raise Exception(
|
|
354
|
+
"The 'data-point' value must be a list of numeric values."
|
|
355
|
+
)
|
|
356
|
+
if data_tags is not None and not isinstance(data_tags, list):
|
|
357
|
+
raise Exception(
|
|
358
|
+
"The optional 'data-tags' value must be a list of strings."
|
|
359
|
+
)
|
|
360
|
+
if not all(isinstance(item, (int, float)) for item in vector):
|
|
361
|
+
raise Exception(
|
|
362
|
+
"The 'data-point' value must be a list of numeric values (int or float)."
|
|
363
|
+
)
|
|
364
|
+
if vector_size is None:
|
|
365
|
+
vector_size = len(vector)
|
|
366
|
+
if len(vector) != vector_size:
|
|
367
|
+
raise Exception(
|
|
368
|
+
"All 'data-point' vectors in inference data entries must have the same length."
|
|
369
|
+
)
|
|
370
|
+
if data_tags is not None and len(data_tags) != vector_size:
|
|
371
|
+
raise Exception(
|
|
372
|
+
"If provided, the 'data-tags' list must have the same length as the 'data-point' vector."
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def validate_quadratic_program(self, qp):
|
|
376
|
+
if not isinstance(qp, QuadraticProgram):
|
|
377
|
+
raise Exception(
|
|
378
|
+
"The input object must be an instance of QuadraticProgram class from Qiskit Optimization module."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def validate_and_serialize_pub(self, pub):
|
|
382
|
+
shots = None
|
|
383
|
+
paramaters = None
|
|
384
|
+
if type(pub) == QuantumCircuit:
|
|
385
|
+
quantum_circuit = pub
|
|
386
|
+
elif type(pub) != tuple:
|
|
387
|
+
raise Exception(
|
|
388
|
+
"A pub can be either a quantum circuit or a tuple containing a quantum circuit, optionally second a list of circuit parameters and optionally third a number of shots."
|
|
389
|
+
)
|
|
390
|
+
elif len(pub) == 3:
|
|
391
|
+
quantum_circuit, paramaters, shots = pub
|
|
392
|
+
elif len(pub) == 2:
|
|
393
|
+
quantum_circuit, paramaters = pub
|
|
394
|
+
elif len(pub) == 1:
|
|
395
|
+
quantum_circuit = pub[0]
|
|
396
|
+
else:
|
|
397
|
+
raise Exception(
|
|
398
|
+
"A pub can be a tuple with at most 3 elements: a quantum circuit, a list of circuit paramaters and a number of shots."
|
|
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."
|
|
407
|
+
)
|
|
408
|
+
if quantum_circuit.num_parameters == 0 and (
|
|
409
|
+
paramaters is not None and len(paramaters) != 0
|
|
410
|
+
):
|
|
411
|
+
raise Exception(
|
|
412
|
+
"A circuit with zero parameters must have 'paramaters' argument 'None' or an empty list."
|
|
413
|
+
)
|
|
414
|
+
elif paramaters is not None and quantum_circuit.num_parameters != len(
|
|
415
|
+
paramaters
|
|
416
|
+
):
|
|
417
|
+
raise Exception(
|
|
418
|
+
f"The number of paramaters for a quantum circuit {quantum_circuit.num_parameters} is different from the length {len(paramaters)} of the list of aruguments."
|
|
419
|
+
)
|
|
420
|
+
if paramaters is not None and not all(
|
|
421
|
+
isinstance(item, (int, float)) for item in paramaters
|
|
422
|
+
):
|
|
423
|
+
raise Exception(
|
|
424
|
+
"The 'paramaters' setting in a PUB must be a list of numbers."
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return (serialize_circuit(quantum_circuit), paramaters, shots)
|
|
428
|
+
|
|
429
|
+
def validate_operator(self, operator):
|
|
430
|
+
if (
|
|
431
|
+
not isinstance(operator, Operator)
|
|
432
|
+
and not isinstance(operator, Pauli)
|
|
433
|
+
and not isinstance(operator, SparsePauliOp)
|
|
434
|
+
and not (
|
|
435
|
+
isinstance(operator, tuple)
|
|
436
|
+
and isinstance(operator[0], PauliList)
|
|
437
|
+
and isinstance(operator[1], list)
|
|
438
|
+
and (operator[1] and not all_numbers(operator[1]))
|
|
439
|
+
)
|
|
440
|
+
):
|
|
441
|
+
raise Exception(
|
|
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
|
+
)
|
|
444
|
+
|
|
445
|
+
if isinstance(operator, Operator):
|
|
446
|
+
matrix = operator.data
|
|
447
|
+
if not np.allclose(matrix, matrix.conj().T):
|
|
448
|
+
print("WARNING: The operator you supplied is not Hermitian!")
|
|
449
|
+
|
|
450
|
+
if (
|
|
451
|
+
isinstance(operator, tuple)
|
|
452
|
+
and isinstance(operator[0], PauliList)
|
|
453
|
+
and isinstance(operator[1], list)
|
|
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
|
+
):
|
|
462
|
+
raise Exception(
|
|
463
|
+
"The number of Pauli terms in the Pauli list must match the number of coefficients or list of coefficients must be empty."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
def to_sparse_pauli_operator(self, operator, coeffs=None):
|
|
467
|
+
if isinstance(operator, SparsePauliOp):
|
|
468
|
+
return operator
|
|
469
|
+
|
|
470
|
+
elif isinstance(operator, Pauli):
|
|
471
|
+
return SparsePauliOp(operator)
|
|
472
|
+
|
|
473
|
+
elif isinstance(operator, PauliList):
|
|
474
|
+
if coeffs is not None and len(coeffs) > 0 and len(coeffs) != len(operator):
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"Number of coefficients must match number of Pauli operators in PauliList"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
coefficients = (
|
|
480
|
+
coeffs
|
|
481
|
+
if (coeffs is not None and len(coeffs) > 0)
|
|
482
|
+
else [1.0] * len(operator)
|
|
483
|
+
)
|
|
484
|
+
pauli_strings = [str(pauli) for pauli in operator]
|
|
485
|
+
return SparsePauliOp(pauli_strings, coeffs=coefficients)
|
|
486
|
+
|
|
487
|
+
elif isinstance(operator, Operator):
|
|
488
|
+
return SparsePauliOp.from_operator(operator)
|
|
489
|
+
|
|
490
|
+
def serialize_sparse_pauli_operator(self, sparse_op):
|
|
491
|
+
if not isinstance(sparse_op, SparsePauliOp):
|
|
492
|
+
raise ValueError("Input must be a SparsePauliOp")
|
|
493
|
+
|
|
494
|
+
pauli_data = sparse_op.to_list()
|
|
495
|
+
pauli_terms = [term[0] for term in pauli_data]
|
|
496
|
+
coefficients = [term[1] for term in pauli_data]
|
|
497
|
+
return pauli_terms, coefficients
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class QuantumFlowsProvider:
|
|
501
|
+
|
|
502
|
+
_asp_net_port_dev = "5001"
|
|
503
|
+
_keycloak_port = "8080"
|
|
504
|
+
_client_id = "straful-client"
|
|
505
|
+
_realm_name = "straful-realm"
|
|
506
|
+
_provider_url_dev = "https://localhost"
|
|
507
|
+
_provider_url_prod = "https://quantum-flows.transilvania-quantum.com"
|
|
508
|
+
_keycloak_url_dev = "http://localhost"
|
|
509
|
+
_keycloak_url_prod = "https://keycloak.transilvania-quantum.com"
|
|
510
|
+
|
|
511
|
+
def __init__(self, use_https=True, debug=False):
|
|
512
|
+
self._use_https = use_https
|
|
513
|
+
self._debug = debug
|
|
514
|
+
self._state = None
|
|
515
|
+
self._access_token = None
|
|
516
|
+
self._refresh_token = None
|
|
517
|
+
self._token_expiration_time = None
|
|
518
|
+
self._refresh_token_expiration_time = None
|
|
519
|
+
self._asp_net_url = (
|
|
520
|
+
f"{self._provider_url_dev}:{self._asp_net_port_dev}"
|
|
521
|
+
if self._debug
|
|
522
|
+
else f"{self._provider_url_prod}"
|
|
523
|
+
)
|
|
524
|
+
self._auth_call_back_url = f"{self._asp_net_url}/auth/callback"
|
|
525
|
+
self._show_code_callback_url = f"{self._asp_net_url}/auth/showcode"
|
|
526
|
+
self._keycloak_server_url = (
|
|
527
|
+
f"{self._keycloak_url_dev}:{self._keycloak_port}"
|
|
528
|
+
if self._debug
|
|
529
|
+
else f"{self._keycloak_url_prod}"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if not self._is_server_online(self._keycloak_server_url):
|
|
533
|
+
raise SystemExit(
|
|
534
|
+
f"The service you are trying to access at: {self._asp_net_url}, is not responding. \
|
|
535
|
+
In case the service has been recently started please wait 5 minutes for it to become fully functional."
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
self._keycloak_openid = KeycloakOpenID(
|
|
539
|
+
server_url=self._keycloak_server_url,
|
|
540
|
+
client_id=self._client_id,
|
|
541
|
+
realm_name=self._realm_name,
|
|
542
|
+
verify=self._use_https,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def authenticate(self):
|
|
546
|
+
try:
|
|
547
|
+
self._access_token = None
|
|
548
|
+
self._refresh_token = None
|
|
549
|
+
self._token_expiration_time = None
|
|
550
|
+
self._refresh_token_expiration_time = None
|
|
551
|
+
self._store_state()
|
|
552
|
+
auth_url = self._get_authentication_url()
|
|
553
|
+
opened = webbrowser.open(auth_url)
|
|
554
|
+
if not opened:
|
|
555
|
+
display(
|
|
556
|
+
HTML(
|
|
557
|
+
f"<p>Please click to authenticate: "
|
|
558
|
+
f'<a href="{auth_url}" target="_blank">{auth_url}</a></p>'
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
auth_code = self._get_authentication_code()
|
|
562
|
+
token_response = self._keycloak_openid.token(
|
|
563
|
+
grant_type="authorization_code",
|
|
564
|
+
code=auth_code,
|
|
565
|
+
redirect_uri=self._auth_call_back_url,
|
|
566
|
+
)
|
|
567
|
+
self._access_token = token_response["access_token"]
|
|
568
|
+
self._refresh_token = token_response["refresh_token"]
|
|
569
|
+
self._token_expiration_time = (
|
|
570
|
+
time.time() + token_response["expires_in"] - 5
|
|
571
|
+
) # seconds
|
|
572
|
+
self._refresh_token_expiration_time = (
|
|
573
|
+
time.time() + token_response["refresh_expires_in"] - 5
|
|
574
|
+
) # seconds
|
|
575
|
+
print("Authentication successful.")
|
|
576
|
+
except AuthenticationFailure as ex:
|
|
577
|
+
print(ex.message)
|
|
578
|
+
except AuthorizationFailure as ex:
|
|
579
|
+
print(
|
|
580
|
+
"Failed to authenticate with the quantum provider. Make sure you are using the correct Gmail account."
|
|
581
|
+
)
|
|
582
|
+
if self._debug:
|
|
583
|
+
print("More details: ", ex.message)
|
|
584
|
+
except Exception as ex:
|
|
585
|
+
print("Failed to authenticate with the quantum provider.")
|
|
586
|
+
if "Connection refused" in str(ex):
|
|
587
|
+
print("The remote service does not respond. Please try again later.")
|
|
588
|
+
if self._debug:
|
|
589
|
+
print("Unexpected exception: ", ex)
|
|
590
|
+
|
|
591
|
+
def submit_job(
|
|
592
|
+
self, *, backend=None, circuit=None, circuits=None, shots=None, comments=""
|
|
593
|
+
):
|
|
594
|
+
if not self._verify_user_is_authenticated():
|
|
595
|
+
return
|
|
596
|
+
if not backend:
|
|
597
|
+
print("Please specify the backend name.")
|
|
598
|
+
return
|
|
599
|
+
if circuit is None and circuits is None:
|
|
600
|
+
print(
|
|
601
|
+
"An quantum circuit to be executed or a list of quantum circuits to be executed must be specified."
|
|
602
|
+
)
|
|
603
|
+
return
|
|
604
|
+
if circuit is not None and circuits is not None:
|
|
605
|
+
print(
|
|
606
|
+
"You can use either 'circuit' or 'circuits' as input arguments but not both at the same time."
|
|
607
|
+
)
|
|
608
|
+
return
|
|
609
|
+
if circuit is not None and not isinstance(circuit, QuantumCircuit):
|
|
610
|
+
print(
|
|
611
|
+
"The 'circuit' argument must be an instance of QuantumCircuit or deriving from it."
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
if circuits is not None and (
|
|
615
|
+
not isinstance(circuits, list)
|
|
616
|
+
or not all(isinstance(circ, QuantumCircuit) for circ in circuits)
|
|
617
|
+
):
|
|
618
|
+
print(
|
|
619
|
+
"The 'circuits' argument must be a list of QuantumCircuit instances or objects deriving from QuantumCircuit."
|
|
620
|
+
)
|
|
621
|
+
return
|
|
622
|
+
if shots is None:
|
|
623
|
+
print("Please specify the number of shots.")
|
|
624
|
+
return
|
|
625
|
+
if not isinstance(shots, int):
|
|
626
|
+
print("The number of shots must be specified as an integer number.")
|
|
627
|
+
return
|
|
628
|
+
try:
|
|
629
|
+
if circuit is not None:
|
|
630
|
+
job_data = {
|
|
631
|
+
"BackendName": backend,
|
|
632
|
+
"Circuit": serialize_circuit(circuit),
|
|
633
|
+
"Circuits": [],
|
|
634
|
+
"Shots": shots,
|
|
635
|
+
"Comments": comments,
|
|
636
|
+
"QiskitVersion": qiskit.__version__,
|
|
637
|
+
}
|
|
638
|
+
elif circuits is not None:
|
|
639
|
+
job_data = {
|
|
640
|
+
"BackendName": backend,
|
|
641
|
+
"Circuit": None,
|
|
642
|
+
"Circuits": [serialize_circuit(circuit) for circuit in circuits],
|
|
643
|
+
"Shots": shots,
|
|
644
|
+
"Comments": comments,
|
|
645
|
+
"QiskitVersion": qiskit.__version__,
|
|
646
|
+
}
|
|
647
|
+
(status_code, result) = self._make_post_request(
|
|
648
|
+
f"{self._asp_net_url}/api/job", job_data
|
|
649
|
+
)
|
|
650
|
+
if status_code == 201:
|
|
651
|
+
return Job(result["id"])
|
|
652
|
+
elif status_code == 401:
|
|
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:
|
|
657
|
+
print(
|
|
658
|
+
"The remote service is currently under maintenance. Please try again later."
|
|
659
|
+
)
|
|
660
|
+
else:
|
|
661
|
+
print(
|
|
662
|
+
f"Job submission has failed with http status code: {status_code}. \nRemote server response: '{result}'"
|
|
663
|
+
)
|
|
664
|
+
return Job(None)
|
|
665
|
+
except Exception as ex:
|
|
666
|
+
print(str(ex))
|
|
667
|
+
|
|
668
|
+
def submit_workflow_job(
|
|
669
|
+
self,
|
|
670
|
+
*,
|
|
671
|
+
backend=None,
|
|
672
|
+
shots=None,
|
|
673
|
+
workflow_id=None,
|
|
674
|
+
comments="",
|
|
675
|
+
max_iterations=None,
|
|
676
|
+
input_data=InputData(),
|
|
677
|
+
):
|
|
678
|
+
if not self._verify_user_is_authenticated():
|
|
679
|
+
return
|
|
680
|
+
if not backend:
|
|
681
|
+
print("Please specify a backend name.")
|
|
682
|
+
return
|
|
683
|
+
if not workflow_id:
|
|
684
|
+
print("Please specify a workflow Id.")
|
|
685
|
+
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
|
+
if not self.is_valid_uuid(workflow_id):
|
|
690
|
+
print("The specified workflow Id is not a valid GUID.")
|
|
691
|
+
return
|
|
692
|
+
if max_iterations is not None:
|
|
693
|
+
if not isinstance(max_iterations, int) or max_iterations <= 0:
|
|
694
|
+
print(
|
|
695
|
+
"The optional 'max-iterations' input argument must be a positive integer."
|
|
696
|
+
)
|
|
697
|
+
return
|
|
698
|
+
try:
|
|
699
|
+
input_data_labels = []
|
|
700
|
+
input_data_items = []
|
|
701
|
+
input_data_labels.append("backend")
|
|
702
|
+
input_data_items.append(backend)
|
|
703
|
+
input_data_labels.append("shots")
|
|
704
|
+
input_data_items.append(str(shots))
|
|
705
|
+
input_data_labels.append("max-iterations")
|
|
706
|
+
input_data_items.append(str(max_iterations))
|
|
707
|
+
for input_data_label in input_data.data.keys():
|
|
708
|
+
input_data_labels.append(input_data_label)
|
|
709
|
+
content = input_data.data[input_data_label]
|
|
710
|
+
input_data_items.append(
|
|
711
|
+
json.dumps(content, indent=2, cls=CustomJSONEncoder)
|
|
712
|
+
)
|
|
713
|
+
job_data = {
|
|
714
|
+
"BackendName": backend,
|
|
715
|
+
"WorkflowId": workflow_id,
|
|
716
|
+
"Shots": shots,
|
|
717
|
+
"Comments": comments,
|
|
718
|
+
"MaxIterations": max_iterations,
|
|
719
|
+
"InputDataLabels": input_data_labels,
|
|
720
|
+
"InputDataItems": input_data_items,
|
|
721
|
+
"QiskitVersion": qiskit.__version__,
|
|
722
|
+
}
|
|
723
|
+
(status_code, result) = self._make_post_request(
|
|
724
|
+
f"{self._asp_net_url}/api/workflow-job", job_data
|
|
725
|
+
)
|
|
726
|
+
if status_code == 201:
|
|
727
|
+
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
|
+
else:
|
|
733
|
+
print(
|
|
734
|
+
f"Workflow job submission has failed with http status code: {status_code}. \nRemote server response: '{result}'"
|
|
735
|
+
)
|
|
736
|
+
return WorkflowJob(None)
|
|
737
|
+
except Exception as ex:
|
|
738
|
+
print(str(ex))
|
|
739
|
+
|
|
740
|
+
def get_backends(self):
|
|
741
|
+
if not self._verify_user_is_authenticated():
|
|
742
|
+
return
|
|
743
|
+
try:
|
|
744
|
+
response = self._make_get_request(f"{self._asp_net_url}/api/backends")
|
|
745
|
+
status_code = response.status_code
|
|
746
|
+
if status_code == 200:
|
|
747
|
+
backends = response.json()
|
|
748
|
+
for backend in backends["$values"]:
|
|
749
|
+
print(
|
|
750
|
+
backend["name"],
|
|
751
|
+
"-",
|
|
752
|
+
f"no qubits: {backend['noQubits']}",
|
|
753
|
+
"-",
|
|
754
|
+
"Online" if backend["online"] else "Offline",
|
|
755
|
+
)
|
|
756
|
+
else:
|
|
757
|
+
print(f"Request has failed with http status code: {status_code}.")
|
|
758
|
+
except Exception as ex:
|
|
759
|
+
print(str(ex))
|
|
760
|
+
|
|
761
|
+
def get_job_status(self, job):
|
|
762
|
+
if not self._verify_user_is_authenticated():
|
|
763
|
+
return
|
|
764
|
+
if job is None or job.id is None:
|
|
765
|
+
print("This job is not valid.")
|
|
766
|
+
return
|
|
767
|
+
if type(job) == Job:
|
|
768
|
+
try:
|
|
769
|
+
response = self._make_get_request(
|
|
770
|
+
f"{self._asp_net_url}/api/job/status/{job.id()}"
|
|
771
|
+
)
|
|
772
|
+
status_code = response.status_code
|
|
773
|
+
if status_code == 200:
|
|
774
|
+
print("Job status: ", response.text)
|
|
775
|
+
else:
|
|
776
|
+
print(f"Request has failed with http status code: {status_code}.")
|
|
777
|
+
except Exception as ex:
|
|
778
|
+
print(str(ex))
|
|
779
|
+
elif type(job) == WorkflowJob:
|
|
780
|
+
try:
|
|
781
|
+
response = self._make_get_request(
|
|
782
|
+
f"{self._asp_net_url}/api/workflow-job/status/{job.id()}"
|
|
783
|
+
)
|
|
784
|
+
status_code = response.status_code
|
|
785
|
+
if status_code == 200:
|
|
786
|
+
print("Job status: ", response.text)
|
|
787
|
+
else:
|
|
788
|
+
print(f"Request has failed with http status code: {status_code}.")
|
|
789
|
+
except Exception as ex:
|
|
790
|
+
print(str(ex))
|
|
791
|
+
|
|
792
|
+
def get_job_result(self, job):
|
|
793
|
+
if not self._verify_user_is_authenticated():
|
|
794
|
+
return
|
|
795
|
+
if job is None or job.id is None:
|
|
796
|
+
print("This job is not valid.")
|
|
797
|
+
return
|
|
798
|
+
if type(job) == Job:
|
|
799
|
+
try:
|
|
800
|
+
response = self._make_get_request(
|
|
801
|
+
f"{self._asp_net_url}/api/job/result/{job.id()}"
|
|
802
|
+
)
|
|
803
|
+
status_code = response.status_code
|
|
804
|
+
if status_code == 200:
|
|
805
|
+
print(response.text)
|
|
806
|
+
else:
|
|
807
|
+
print(f"Request has failed with http status code: {status_code}.")
|
|
808
|
+
except Exception as ex:
|
|
809
|
+
print(str(ex))
|
|
810
|
+
elif type(job) == WorkflowJob:
|
|
811
|
+
print("Operation not supported for workflow jobs.")
|
|
812
|
+
|
|
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
|
+
def _make_get_request(self, api_url):
|
|
829
|
+
if self.is_token_expired():
|
|
830
|
+
self._try_refresh_tokens()
|
|
831
|
+
return requests.get(
|
|
832
|
+
api_url,
|
|
833
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
834
|
+
verify=self._use_https,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
def _make_post_request(self, api_url, data):
|
|
838
|
+
if self.is_token_expired():
|
|
839
|
+
self._try_refresh_tokens()
|
|
840
|
+
response = requests.post(
|
|
841
|
+
api_url,
|
|
842
|
+
json=data,
|
|
843
|
+
headers={"Authorization": f"Bearer {self._access_token}"},
|
|
844
|
+
verify=self._use_https,
|
|
845
|
+
)
|
|
846
|
+
try:
|
|
847
|
+
json = response.json()
|
|
848
|
+
return (response.status_code, json)
|
|
849
|
+
except:
|
|
850
|
+
return (response.status_code, response.text)
|
|
851
|
+
|
|
852
|
+
def is_token_expired(self):
|
|
853
|
+
if self._token_expiration_time is None:
|
|
854
|
+
return True
|
|
855
|
+
return time.time() > self._token_expiration_time
|
|
856
|
+
|
|
857
|
+
def is_refresh_token_expired(self):
|
|
858
|
+
if self._refresh_token_expiration_time is None:
|
|
859
|
+
return True
|
|
860
|
+
return time.time() > self._refresh_token_expiration_time
|
|
861
|
+
|
|
862
|
+
def _try_refresh_tokens(self):
|
|
863
|
+
try:
|
|
864
|
+
token_response = self._keycloak_openid.token(
|
|
865
|
+
grant_type="refresh_token", refresh_token=self._refresh_token
|
|
866
|
+
)
|
|
867
|
+
self._access_token = token_response["access_token"]
|
|
868
|
+
self._refresh_token = token_response["refresh_token"]
|
|
869
|
+
self._token_expiration_time = (
|
|
870
|
+
time.time() + token_response["expires_in"] - 5
|
|
871
|
+
) # seconds
|
|
872
|
+
self._refresh_token_expiration_time = (
|
|
873
|
+
time.time() + token_response["refresh_expires_in"] - 5
|
|
874
|
+
) # seconds
|
|
875
|
+
except:
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
def _is_server_online(self, url):
|
|
879
|
+
try:
|
|
880
|
+
response = requests.get(url, verify=self._use_https)
|
|
881
|
+
if response.status_code == 200:
|
|
882
|
+
return True
|
|
883
|
+
return False
|
|
884
|
+
except requests.exceptions.RequestException as e:
|
|
885
|
+
return False
|
|
886
|
+
|
|
887
|
+
def _is_server_under_maintenance(self, url):
|
|
888
|
+
try:
|
|
889
|
+
response = requests.get(url, verify=self._use_https)
|
|
890
|
+
if "Under Maintenance" in response.text:
|
|
891
|
+
return True
|
|
892
|
+
return False
|
|
893
|
+
except requests.exceptions.RequestException as e:
|
|
894
|
+
return False
|
|
895
|
+
|
|
896
|
+
def _store_state(self):
|
|
897
|
+
state = secrets.token_urlsafe(64)
|
|
898
|
+
response = requests.post(
|
|
899
|
+
f"{self._asp_net_url}/auth/storestate",
|
|
900
|
+
json={"state": state},
|
|
901
|
+
verify=self._use_https,
|
|
902
|
+
)
|
|
903
|
+
if response.status_code != 200:
|
|
904
|
+
if not self._is_server_online(self._asp_net_url):
|
|
905
|
+
raise AuthenticationFailure(
|
|
906
|
+
f"The service you are trying to access at: {self._asp_net_url} is not online."
|
|
907
|
+
)
|
|
908
|
+
elif self._is_server_under_maintenance(self._asp_net_url):
|
|
909
|
+
raise AuthenticationFailure(
|
|
910
|
+
f"The service you are trying to access at: {self._asp_net_url} is under maintenance."
|
|
911
|
+
)
|
|
912
|
+
else:
|
|
913
|
+
raise AuthenticationFailure(
|
|
914
|
+
"Cannot initiate authentication, the authentication provider does not respond."
|
|
915
|
+
)
|
|
916
|
+
self._state = state
|
|
917
|
+
|
|
918
|
+
def _get_authentication_url(self):
|
|
919
|
+
auth_url_params = {
|
|
920
|
+
"client_id": self._client_id,
|
|
921
|
+
"redirect_uri": self._auth_call_back_url,
|
|
922
|
+
"response_type": "code",
|
|
923
|
+
"scope": "openid profile email",
|
|
924
|
+
"kc_idp_hint": "google",
|
|
925
|
+
"state": self._state,
|
|
926
|
+
}
|
|
927
|
+
return f"{self._keycloak_server_url}/realms/{self._realm_name}/protocol/openid-connect/auth?{urlencode(auth_url_params)}"
|
|
928
|
+
|
|
929
|
+
def _get_authentication_code(self):
|
|
930
|
+
|
|
931
|
+
timeout_seconds = 16
|
|
932
|
+
start_time = time.time()
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
while (time.time() - start_time) < timeout_seconds:
|
|
936
|
+
response = requests.get(
|
|
937
|
+
self._show_code_callback_url,
|
|
938
|
+
params={"state": self._state},
|
|
939
|
+
verify=self._use_https,
|
|
940
|
+
)
|
|
941
|
+
# TODO: what if I use a wrong email account
|
|
942
|
+
if response.status_code == 400:
|
|
943
|
+
if response.text == "Authorization state is missing.":
|
|
944
|
+
raise Exception()
|
|
945
|
+
time.sleep(1)
|
|
946
|
+
continue
|
|
947
|
+
data = response.text
|
|
948
|
+
auth_code = data.split(": ")[1]
|
|
949
|
+
return auth_code
|
|
950
|
+
except Exception as e:
|
|
951
|
+
if self._debug:
|
|
952
|
+
print(
|
|
953
|
+
f"Failed to retrieve the authorization code from the authentication provider: {e}"
|
|
954
|
+
)
|
|
955
|
+
pass
|
|
956
|
+
|
|
957
|
+
raise AuthorizationFailure(
|
|
958
|
+
"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
|
+
)
|
|
960
|
+
|
|
961
|
+
def is_valid_uuid(self, value: str) -> bool:
|
|
962
|
+
try:
|
|
963
|
+
uuid_obj = uuid.UUID(value)
|
|
964
|
+
return str(uuid_obj) == value.lower()
|
|
965
|
+
except (ValueError, TypeError):
|
|
966
|
+
return False
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
quantum_flows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
quantum_flows/quantum_flows.py,sha256=gupYjqTeNm-0hVHc9oH2Udc7bCgymH5ed-MJjFkrpmA,39224
|
|
3
|
+
quantum_flows/quantum_flows.py.bck,sha256=ak5TNi4uCzuUPJn_GAQpPoH_XfaVvp85C844d8xeFbA,39179
|
|
4
|
+
quantum_flows-0.1.15.dist-info/LICENSE,sha256=IMxjvLtrzufAKrJSQ5u9vkcL3LbqJnOU8He5HXDet3Q,1077
|
|
5
|
+
quantum_flows-0.1.15.dist-info/METADATA,sha256=U3WNKJ6CPdoDR3PuGJJKMcieP36oMdQCmsOX7IzhnM4,1063
|
|
6
|
+
quantum_flows-0.1.15.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
7
|
+
quantum_flows-0.1.15.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
quantum_flows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
quantum_flows/quantum_flows.py,sha256=ak5TNi4uCzuUPJn_GAQpPoH_XfaVvp85C844d8xeFbA,39179
|
|
3
|
-
quantum_flows-0.1.14.dist-info/LICENSE,sha256=IMxjvLtrzufAKrJSQ5u9vkcL3LbqJnOU8He5HXDet3Q,1077
|
|
4
|
-
quantum_flows-0.1.14.dist-info/METADATA,sha256=FKmmrt9ASrWUjbIxx7GE4eBSI9OFrQSdeB1RJhajOJQ,1063
|
|
5
|
-
quantum_flows-0.1.14.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
6
|
-
quantum_flows-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|