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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantum-flows
3
- Version: 0.1.19
3
+ Version: 0.1.21
4
4
  Summary: A python library for interacting with Transilvania-Quantum Quantum Flows quantum computing API backbone.
5
5
  License: MIT
6
6
  Author: Radu Marginean
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "quantum-flows"
3
- version = "0.1.19"
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
- # Convert enum to string
43
- return str(obj)
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 all_numbers(lst):
48
- return all(isinstance(x, (int, float, complex)) for x in lst)
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
- def __init__(self, message):
61
- self.message = message
81
+ pass
62
82
 
63
83
 
64
84
  class AuthorizationFailure(Exception):
65
- def __init__(self, message):
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
- return json.dumps(self.data, indent=2, cls=CustomJSONEncoder)
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(operator)
100
- coeffs = None
101
- if type(content) == tuple:
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 == "pub":
159
+ elif label == "pubs":
115
160
  content = self.validate_and_serialize_pub(content)
116
- if not "pubs" in self.data.keys():
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
- self.data[label] = self.lattice_to_dict(content)
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
- except (OverflowError, TypeError, ValueError):
140
- raise Exception("Input data content must be JSON serializable.")
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 type(label) != str:
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-iterations",
208
+ "max-fun-evaluations",
152
209
  "molecule-info",
153
210
  "operator",
154
- "pub",
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 != "pub" and label in data.keys():
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": lattice.boundary_condition[0].name,
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": lattice.boundary_condition.name,
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["symbols"], list):
233
- raise Exception("The 'symbols' must be a list of nuclei.")
234
- if not isinstance(molecule_info["coords"], list):
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 "mutiplicity" in molecule_info and not isinstance(
239
- molecule_info["mutiplicity"], int
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 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
- )
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 "label" in data:
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
- "Each dictionary in the list of training data points must contain a 'label' key."
431
+ f"If provided, the 'data-tags' list must contain only strings (check line {line})."
332
432
  )
333
- label = data["label"]
334
- if not isinstance(label, (int, float)):
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' value must be a numeric type (int or float)."
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 and not isinstance(data_tags, list):
357
- raise Exception(
358
- "The optional 'data-tags' value must be a list of strings."
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
- paramaters = None
384
- if type(pub) == QuantumCircuit:
531
+ parameters = None
532
+
533
+ if isinstance(pub, QuantumCircuit):
385
534
  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]
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 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."
548
+ "A pub must be a QuantumCircuit or a tuple (circuit[, params[, shots]])."
407
549
  )
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
- ):
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
- 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."
562
+ "Circuit has zero parameters; parameters must be None or []."
419
563
  )
420
- if paramaters is not None and not all(
421
- isinstance(item, (int, float)) for item in paramaters
422
- ):
564
+
565
+ if parameters is not None and quantum_circuit.num_parameters != len(parameters):
423
566
  raise Exception(
424
- "The 'paramaters' setting in a PUB must be a list of numbers."
567
+ f"Parameter count mismatch: circuit expects {quantum_circuit.num_parameters}, got {len(parameters)}."
425
568
  )
426
569
 
427
- return (serialize_circuit(quantum_circuit), paramaters, shots)
570
+ return (serialize_circuit(quantum_circuit), parameters, shots)
428
571
 
429
572
  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
- )
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
- 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!")
586
+ pauli_list, coeffs = operator
587
+ if coeffs is None:
588
+ return
449
589
 
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
- ):
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
- "The number of Pauli terms in the Pauli list must match the number of coefficients or list of coefficients must be empty."
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
- 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
- )
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[0] for term in pauli_data]
496
- coefficients = [term[1] for term in pauli_data]
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, use_https=True, debug=False):
512
- self._use_https = use_https
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._use_https,
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.message)
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(circuit) for circuit in circuits],
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 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:
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
- max_iterations=None,
676
- input_data=InputData(),
853
+ max_fun_evaluations=None,
854
+ input_data=None,
677
855
  ):
678
- if not self._verify_user_is_authenticated():
679
- return
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 max_iterations is not None:
693
- if not isinstance(max_iterations, int) or max_iterations <= 0:
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-iterations' input argument must be a positive integer."
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-iterations")
706
- input_data_items.append(str(max_iterations))
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
- input_data_items.append(
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
- "MaxIterations": max_iterations,
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 not self._verify_user_is_authenticated():
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 type(job) == Job:
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 type(job) == WorkflowJob:
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 not self._verify_user_is_authenticated():
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 type(job) == Job:
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 type(job) == WorkflowJob:
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
- if self.is_token_expired():
830
- self._try_refresh_tokens()
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._use_https,
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
- if self.is_token_expired():
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._use_https,
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
- json = response.json()
848
- return (response.status_code, json)
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 _try_refresh_tokens(self):
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", refresh_token=self._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
- ) # seconds
875
- except:
876
- pass
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
- response = requests.get(url, verify=self._use_https)
881
- if response.status_code == 200:
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(url, verify=self._use_https)
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._use_https,
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._use_https,
1231
+ verify=self._verify_tls,
1232
+ timeout=DEFAULT_TIMEOUT,
940
1233
  )
941
- # TODO: what if I use a wrong email account
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
- data = response.text
948
- auth_code = data.split(": ")[1]
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: str) -> bool:
1264
+ def is_valid_uuid(self, value) -> bool:
1265
+ if not isinstance(value, str):
1266
+ return False
962
1267
  try:
963
- uuid_obj = uuid.UUID(value)
964
- return str(uuid_obj) == value.lower()
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