iqm-client 31.7.0__py3-none-any.whl → 32.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iqm/iqm_client/authentication.py +0 -3
- iqm/iqm_client/models.py +3 -0
- iqm/qiskit_iqm/examples/bell_measure.py +42 -17
- iqm/qiskit_iqm/fake_backends/fake_adonis.py +1 -0
- iqm/qiskit_iqm/fake_backends/fake_aphrodite.py +1 -0
- iqm/qiskit_iqm/fake_backends/fake_apollo.py +1 -0
- iqm/qiskit_iqm/fake_backends/fake_deneb.py +1 -0
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/METADATA +1 -1
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/RECORD +14 -20
- iqm/iqm_client/cli/__init__.py +0 -14
- iqm/iqm_client/cli/auth.py +0 -168
- iqm/iqm_client/cli/cli.py +0 -798
- iqm/iqm_client/cli/models.py +0 -40
- iqm/iqm_client/cli/token_manager.py +0 -196
- iqm/qiskit_iqm/examples/resonance_example.py +0 -83
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/AUTHORS.rst +0 -0
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/LICENSE.txt +0 -0
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/WHEEL +0 -0
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/entry_points.txt +0 -0
- {iqm_client-31.7.0.dist-info → iqm_client-32.0.0.dist-info}/top_level.txt +0 -0
iqm/iqm_client/authentication.py
CHANGED
|
@@ -22,9 +22,6 @@ from typing import Any
|
|
|
22
22
|
|
|
23
23
|
from iqm.iqm_client.errors import ClientAuthenticationError, ClientConfigurationError
|
|
24
24
|
|
|
25
|
-
AUTH_CLIENT_ID = "iqm_client"
|
|
26
|
-
AUTH_REALM = "cortex"
|
|
27
|
-
AUTH_REQUESTS_TIMEOUT = float(os.environ.get("IQM_CLIENT_REQUESTS_TIMEOUT", 60.0))
|
|
28
25
|
REFRESH_MARGIN_SECONDS = 60
|
|
29
26
|
|
|
30
27
|
|
iqm/iqm_client/models.py
CHANGED
|
@@ -390,6 +390,9 @@ class StaticQuantumArchitecture(BaseModel):
|
|
|
390
390
|
including the names of its computational components and the connections between them.
|
|
391
391
|
"""
|
|
392
392
|
|
|
393
|
+
# Optional *for now* (backwards compatible)
|
|
394
|
+
dut_label: str | None = None
|
|
395
|
+
"""Identifies the QPU."""
|
|
393
396
|
qubits: list[str] = Field(...)
|
|
394
397
|
"""Names of the physical qubits on the QPU, sorted."""
|
|
395
398
|
computational_resonators: list[str] = Field(...)
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
"""
|
|
15
|
-
|
|
14
|
+
"""An example on using Qiskit on IQM to run a simple quantum circuit on an IQM quantum computer.
|
|
15
|
+
|
|
16
|
+
See the Qiskit on IQM user guide for instructions:
|
|
16
17
|
https://docs.meetiqm.com/iqm-client/user_guide_qiskit.html
|
|
17
18
|
"""
|
|
18
19
|
|
|
@@ -22,34 +23,58 @@ from iqm.qiskit_iqm.iqm_provider import IQMProvider
|
|
|
22
23
|
from qiskit import QuantumCircuit, transpile
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def bell_measure(server_url: str) -> dict[str, int]:
|
|
26
|
-
"""
|
|
26
|
+
def bell_measure(server_url: str, token: str | None = None, shots: int = 1000) -> dict[str, int]:
|
|
27
|
+
"""Execute a quantum circuit that prepares and measures a generalized Bell (aka GHZ) state.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
29
30
|
server_url: URL of the IQM server used for execution
|
|
31
|
+
token: API token for authentication. If not given, uses :env:`IQM_TOKEN`.
|
|
32
|
+
shots: Requested number of shots.
|
|
30
33
|
|
|
31
34
|
Returns:
|
|
32
|
-
|
|
35
|
+
Mapping of bitstrings representing qubit measurement results to counts for each result.
|
|
33
36
|
|
|
34
37
|
"""
|
|
35
|
-
|
|
38
|
+
print(f"Executing a circuit on {server_url}")
|
|
39
|
+
# Initialize a backend without metrics as IQMClient._get_calibration_quality_metrics is not supported by resonance
|
|
40
|
+
backend = IQMProvider(server_url, token=token).get_backend()
|
|
36
41
|
if backend.num_qubits < 2:
|
|
37
42
|
raise ValueError("We need two qubits for the Bell state.")
|
|
38
|
-
circuit = QuantumCircuit(2)
|
|
39
|
-
circuit.h(0)
|
|
40
|
-
circuit.cx(0, 1)
|
|
41
|
-
circuit.measure_all()
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
# Just to make sure that "get_static_quantum_architecture" method works
|
|
45
|
+
static_quantum_architecture = backend.client.get_static_quantum_architecture()
|
|
46
|
+
print(f"static_quantum_architecture={static_quantum_architecture}")
|
|
47
|
+
|
|
48
|
+
# Define a quantum circuit for a GHZ state
|
|
49
|
+
n_qubits = min(backend.num_qubits, 5) # use at most 5 qubits
|
|
50
|
+
qc = QuantumCircuit(n_qubits)
|
|
51
|
+
qc.h(0)
|
|
52
|
+
for qb in range(1, n_qubits):
|
|
53
|
+
qc.cx(0, qb)
|
|
54
|
+
qc.barrier()
|
|
55
|
+
qc.measure_all()
|
|
56
|
+
|
|
57
|
+
# Transpile the circuit
|
|
58
|
+
qc_transpiled = transpile(qc, backend)
|
|
59
|
+
print(qc_transpiled.draw(output="text"))
|
|
60
|
+
|
|
61
|
+
# Run the circuit
|
|
62
|
+
job = backend.run(qc_transpiled, shots=shots)
|
|
63
|
+
return job.result().get_counts()
|
|
45
64
|
|
|
46
65
|
|
|
47
66
|
if __name__ == "__main__":
|
|
48
67
|
argparser = argparse.ArgumentParser()
|
|
49
68
|
argparser.add_argument(
|
|
50
|
-
"--url",
|
|
51
|
-
help="IQM server URL",
|
|
52
|
-
# For example https://cocos.resonance.meetiqm.com/garnet
|
|
53
|
-
default="https://<IQM SERVER>",
|
|
69
|
+
"--url", required=True, help='IQM server URL, for example "https://cocos.resonance.meetiqm.com/garnet"'
|
|
54
70
|
)
|
|
55
|
-
|
|
71
|
+
argparser.add_argument(
|
|
72
|
+
"--token",
|
|
73
|
+
help="API token for authentication",
|
|
74
|
+
# Provide the API token explicitly or set it as an environment variable
|
|
75
|
+
# following the https://docs.meetiqm.com/iqm-client/user_guide_qiskit.html#authentication
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
args = argparser.parse_args()
|
|
79
|
+
counts = bell_measure(args.url, args.token)
|
|
80
|
+
print(counts)
|
|
@@ -20,6 +20,7 @@ from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMErrorProfile, IQMFa
|
|
|
20
20
|
def IQMFakeAdonis() -> IQMFakeBackend:
|
|
21
21
|
"""Return IQMFakeBackend instance representing IQM's Adonis architecture."""
|
|
22
22
|
architecture = StaticQuantumArchitecture(
|
|
23
|
+
dut_label="M138_W0_A22_Z99",
|
|
23
24
|
qubits=["QB1", "QB2", "QB3", "QB4", "QB5"],
|
|
24
25
|
computational_resonators=[],
|
|
25
26
|
connectivity=[("QB1", "QB3"), ("QB2", "QB3"), ("QB3", "QB4"), ("QB3", "QB5")],
|
|
@@ -20,6 +20,7 @@ from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMErrorProfile, IQMFa
|
|
|
20
20
|
def IQMFakeAphrodite() -> IQMFakeBackend:
|
|
21
21
|
"""Return IQMFakeBackend instance representing IQM's Aphrodite architecture."""
|
|
22
22
|
architecture = StaticQuantumArchitecture(
|
|
23
|
+
dut_label="M213_W0_H03_Z99",
|
|
23
24
|
qubits=[f"QB{i}" for i in range(1, 55)],
|
|
24
25
|
computational_resonators=[],
|
|
25
26
|
connectivity=[
|
|
@@ -20,6 +20,7 @@ from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMErrorProfile, IQMFa
|
|
|
20
20
|
def IQMFakeApollo() -> IQMFakeBackend:
|
|
21
21
|
"""Return IQMFakeBackend instance representing IQM's Apollo architecture."""
|
|
22
22
|
architecture = StaticQuantumArchitecture(
|
|
23
|
+
dut_label="M153_W0_P06_Z99",
|
|
23
24
|
qubits=[f"QB{i}" for i in range(1, 21)],
|
|
24
25
|
computational_resonators=[],
|
|
25
26
|
connectivity=[
|
|
@@ -20,6 +20,7 @@ from iqm.qiskit_iqm.fake_backends.iqm_fake_backend import IQMErrorProfile, IQMFa
|
|
|
20
20
|
def IQMFakeDeneb() -> IQMFakeBackend:
|
|
21
21
|
"""Return IQMFakeBackend instance representing IQM's Deneb architecture."""
|
|
22
22
|
architecture = StaticQuantumArchitecture(
|
|
23
|
+
dut_label="M139_W0_N60_Z99",
|
|
23
24
|
qubits=["QB1", "QB2", "QB3", "QB4", "QB5", "QB6"],
|
|
24
25
|
computational_resonators=["CR1"],
|
|
25
26
|
connectivity=[
|
|
@@ -19,19 +19,14 @@ iqm/cirq_iqm/examples/demo_iqm_execution.py,sha256=I9hh1DfveBky-ngJlLmuxucIzlGtR
|
|
|
19
19
|
iqm/cirq_iqm/examples/usage.ipynb,sha256=Kyfyu_MwqzTavHVNjgrWJo1tZPeJwTw7ExcU0MFYNRk,34208
|
|
20
20
|
iqm/iqm_client/__init__.py,sha256=D-8W54EcQIxk_1JZo_86GYlR1YitHhPIiFwwLJ2IfGE,1411
|
|
21
21
|
iqm/iqm_client/api.py,sha256=_c6OVuv2dyzBF7J2XlK_qxisTSPyOiI4gYokZPsuaJY,3083
|
|
22
|
-
iqm/iqm_client/authentication.py,sha256=
|
|
22
|
+
iqm/iqm_client/authentication.py,sha256=VtndzZ-nxuaU7q2G8bXYctGThcTuFZW0PqMNE9k4OCA,7447
|
|
23
23
|
iqm/iqm_client/errors.py,sha256=ty2P-sg80zlAoL3_kC3PlprgDUv4PI-KFhmmxaaapS0,1429
|
|
24
24
|
iqm/iqm_client/iqm_client.py,sha256=oX_wu7aIJCdwc-DZy8iF43n4t7mphtp7ha5Ec38kOkM,41160
|
|
25
|
-
iqm/iqm_client/models.py,sha256=
|
|
25
|
+
iqm/iqm_client/models.py,sha256=0i0jYI8G_mJlNGCbH9W89OBS3JeqbcD2n_kUcM1AqIs,41942
|
|
26
26
|
iqm/iqm_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
iqm/iqm_client/transpile.py,sha256=2hDxQK8F4eMgpJC06XMIA8YbguFnRVNMlE1iKQNiTZk,37268
|
|
28
28
|
iqm/iqm_client/util.py,sha256=obzh1g6PNEXOj7k3gUkiylNUhyqutbWlxlEpfyyU_fk,1505
|
|
29
29
|
iqm/iqm_client/validation.py,sha256=GAe5GQAEXyplbmS_ioIRLR4fhCYKSranB0vE06qMfe8,12192
|
|
30
|
-
iqm/iqm_client/cli/__init__.py,sha256=zzLDDz5rc3lJke3OKU8zxR5zQyQoM9oI2bLJ2YKk_zQ,692
|
|
31
|
-
iqm/iqm_client/cli/auth.py,sha256=kESEK9-vpEhrjba3Lb6Wqx24yGfbjxUASeCArnVRYrw,6364
|
|
32
|
-
iqm/iqm_client/cli/cli.py,sha256=vdhRJPKbqKRL0D_Z0uc3V73jQKKftAKE5Hx44oOCBwA,28689
|
|
33
|
-
iqm/iqm_client/cli/models.py,sha256=Hu-t6c_07Cth3AuQBo0CDTcWVQg1xbJCpy_94V0o64U,1199
|
|
34
|
-
iqm/iqm_client/cli/token_manager.py,sha256=125uRj8kBzKlWAhQWNf-8n-aDG6fQridVd95qCktzD4,6867
|
|
35
30
|
iqm/qiskit_iqm/__init__.py,sha256=Mv9V_r8ZcmGC8Ke5S8-7yLOx02vjZ1qiVx8mtbOpwnY,1420
|
|
36
31
|
iqm/qiskit_iqm/iqm_backend.py,sha256=HddizT6yHHq-MOG_U48n6ftE9AqmzaqbXYayEC1ljso,5548
|
|
37
32
|
iqm/qiskit_iqm/iqm_circuit.py,sha256=jaPo3zc5FC0vAIumh5d56fr44fDaJXXwcquBzQEy1Yg,1400
|
|
@@ -47,20 +42,19 @@ iqm/qiskit_iqm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
47
42
|
iqm/qiskit_iqm/qiskit_to_iqm.py,sha256=3Sm1gLELFLCLf6QiATZSiPzLzkjvzUakhU2N6KoBXC8,14948
|
|
48
43
|
iqm/qiskit_iqm/transpiler_plugins.py,sha256=iuReGL42fCe5aOoH-KMUsb6t7Ok9qmIIj2S4yotJJ-U,8749
|
|
49
44
|
iqm/qiskit_iqm/examples/__init__.py,sha256=M4ElQHCo-WxtVXK39bF3QiFT3IGXPtZ1khqexHiTBEc,20
|
|
50
|
-
iqm/qiskit_iqm/examples/bell_measure.py,sha256=
|
|
51
|
-
iqm/qiskit_iqm/examples/resonance_example.py,sha256=1byRYxoFRtQcOpWtTzGfCYVyOlwtJvdpFrGo_kMR8ng,2954
|
|
45
|
+
iqm/qiskit_iqm/examples/bell_measure.py,sha256=vlayliApHU70a2zTnN_gs6iKSSdaB4Jip-iQHJNYN5M,3064
|
|
52
46
|
iqm/qiskit_iqm/examples/transpile_example.py,sha256=cQmXXx3lqvmhgWYFK_U5HmHzMOKQqXUXPYfQhkyHH14,2135
|
|
53
47
|
iqm/qiskit_iqm/fake_backends/__init__.py,sha256=fkw2UHT-3aJbAKvR1WYUN7_4N5Gdwpx9bm6vlWj1tm0,874
|
|
54
|
-
iqm/qiskit_iqm/fake_backends/fake_adonis.py,sha256=
|
|
55
|
-
iqm/qiskit_iqm/fake_backends/fake_aphrodite.py,sha256=
|
|
56
|
-
iqm/qiskit_iqm/fake_backends/fake_apollo.py,sha256=
|
|
57
|
-
iqm/qiskit_iqm/fake_backends/fake_deneb.py,sha256=
|
|
48
|
+
iqm/qiskit_iqm/fake_backends/fake_adonis.py,sha256=vdWaVB6Hsa-SYgMWuYBtRQH5x5Y1qVHvdomWT2bh5QQ,2225
|
|
49
|
+
iqm/qiskit_iqm/fake_backends/fake_aphrodite.py,sha256=78KL0sONOtYKsWjPFGdc-uIxRUguncBbRQ9CP6bD_FQ,15510
|
|
50
|
+
iqm/qiskit_iqm/fake_backends/fake_apollo.py,sha256=6YSaKtUc6zZ4m3iAREbmxL4tRs_DW0ulmZy2Sq7sPUc,6600
|
|
51
|
+
iqm/qiskit_iqm/fake_backends/fake_deneb.py,sha256=_xpo_NJdu9t4ER8p5be-xTRmApkFY6kQw1UEGUPizk0,3193
|
|
58
52
|
iqm/qiskit_iqm/fake_backends/fake_garnet.py,sha256=GI0xafTCj1Um09qVuccO6GPOGBm6ygul_O40Wu220Ys,5555
|
|
59
53
|
iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py,sha256=wJtfsxjPYbDKmzaz5R4AuaXvvPHa21WyPtRgNctL9eY,16785
|
|
60
|
-
iqm_client-
|
|
61
|
-
iqm_client-
|
|
62
|
-
iqm_client-
|
|
63
|
-
iqm_client-
|
|
64
|
-
iqm_client-
|
|
65
|
-
iqm_client-
|
|
66
|
-
iqm_client-
|
|
54
|
+
iqm_client-32.0.0.dist-info/AUTHORS.rst,sha256=qsxeK5A3-B_xK3hNbhFHEIkoHNpo7sdzYyRTs7Bdtm8,795
|
|
55
|
+
iqm_client-32.0.0.dist-info/LICENSE.txt,sha256=2DXrmQtVVUV9Fc9RBFJidMiTEaQlG2oAtlC9PMrEwTk,11333
|
|
56
|
+
iqm_client-32.0.0.dist-info/METADATA,sha256=TNcHTh2I8F7zKgtU-Lh1QfNWpmUH1WwhO1SY0pJCyDo,17887
|
|
57
|
+
iqm_client-32.0.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
58
|
+
iqm_client-32.0.0.dist-info/entry_points.txt,sha256=Kk2qfRwk8vbIJ7qCAvmaUogfRRn6t92_hBFhe6kqAE4,1317
|
|
59
|
+
iqm_client-32.0.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
|
|
60
|
+
iqm_client-32.0.0.dist-info/RECORD,,
|
iqm/iqm_client/cli/__init__.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Copyright 2021-2023 IQM client developers
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
"""Command-line interface (CLI) for managing user authentication when using IQM quantum computers."""
|
iqm/iqm_client/cli/auth.py
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Copyright 2021-2023 IQM client developers
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
"""Authorization and session management for IQM Client CLI."""
|
|
15
|
-
|
|
16
|
-
from base64 import b64decode
|
|
17
|
-
from enum import Enum
|
|
18
|
-
import json
|
|
19
|
-
import time
|
|
20
|
-
|
|
21
|
-
from pydantic import BaseModel, Field
|
|
22
|
-
import requests
|
|
23
|
-
|
|
24
|
-
REFRESH_MARGIN_SECONDS = 15
|
|
25
|
-
AUTH_REQUESTS_TIMEOUT = 20
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class ClientAuthenticationError(RuntimeError):
|
|
29
|
-
"""Something went wrong with user authentication."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ClientAccountSetupError(RuntimeError):
|
|
33
|
-
"""User's account has not been fully set up yet."""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class GrantType(str, Enum):
|
|
37
|
-
"""Type of token request."""
|
|
38
|
-
|
|
39
|
-
PASSWORD = "password"
|
|
40
|
-
REFRESH = "refresh_token"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class AuthRequest(BaseModel):
|
|
44
|
-
"""Request sent to authentication server for access token and refresh token, or for terminating the session.
|
|
45
|
-
|
|
46
|
-
* Token request with grant type ``'password'`` starts a new session in the authentication server.
|
|
47
|
-
It uses fields ``client_id``, ``grant_type``, ``username`` and ``password``.
|
|
48
|
-
* Token request with grant type ``'refresh_token'`` is used for maintaining an existing session.
|
|
49
|
-
It uses field ``client_id``, ``grant_type``, ``refresh_token``.
|
|
50
|
-
* Logout request uses only fields ``client_id`` and ``refresh_token``.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
client_id: str = Field(...)
|
|
54
|
-
"name of the client for all request types"
|
|
55
|
-
grant_type: GrantType | None = Field(None)
|
|
56
|
-
"type of token request, in ``{'password', 'refresh_token'}``"
|
|
57
|
-
username: str | None = Field(None)
|
|
58
|
-
"username for grant type ``'password'``"
|
|
59
|
-
password: str | None = Field(None)
|
|
60
|
-
"password for grant type ``'password'``"
|
|
61
|
-
refresh_token: str | None = Field(None)
|
|
62
|
-
"refresh token for grant type ``'refresh_token'`` and logout request"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def slash_join(a: str, b: str) -> str:
|
|
66
|
-
"""Join two URL segments together, ensuring a single slash between them."""
|
|
67
|
-
return a.rstrip("/") + "/" + b.lstrip("/")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def login_request(url: str, realm: str, client_id: str, username: str, password: str) -> dict[str, str]:
|
|
71
|
-
"""Sends login request to the authentication server.
|
|
72
|
-
|
|
73
|
-
Raises:
|
|
74
|
-
ClientAuthenticationError: obtaining the tokens failed
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
Tokens dictionary
|
|
78
|
-
|
|
79
|
-
"""
|
|
80
|
-
data = AuthRequest(
|
|
81
|
-
client_id=client_id, grant_type=GrantType.PASSWORD, username=username, password=password, refresh_token=None
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
request_url = slash_join(url, f"realms/{realm}/protocol/openid-connect/token")
|
|
85
|
-
result = requests.post(request_url, data=data.model_dump(exclude_none=True), timeout=AUTH_REQUESTS_TIMEOUT)
|
|
86
|
-
if result.status_code == 404:
|
|
87
|
-
raise ClientAuthenticationError(f"token endpoint is not available at {url}")
|
|
88
|
-
if result.status_code == 400 and result.json().get("error_description", "") == "Account is not fully set up":
|
|
89
|
-
raise ClientAccountSetupError("Account is not fully set up")
|
|
90
|
-
if result.status_code != 200:
|
|
91
|
-
raise ClientAuthenticationError("invalid username and/or password")
|
|
92
|
-
tokens = result.json()
|
|
93
|
-
tokens = {key: tokens.get(key, "") for key in ["access_token", "refresh_token"]}
|
|
94
|
-
return tokens
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def refresh_request(url: str, realm: str, client_id: str, refresh_token: str) -> dict[str, str] | None:
|
|
98
|
-
"""Sends refresh request to the authentication server.
|
|
99
|
-
|
|
100
|
-
Raises:
|
|
101
|
-
Timeout: no response from auth server within the timeout period
|
|
102
|
-
ConnectionError: connecting the auth server failed on all retries
|
|
103
|
-
ClientAuthenticationError: updating the tokens failed
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
Tokens dictionary, or None if refresh_token is expired.
|
|
107
|
-
|
|
108
|
-
"""
|
|
109
|
-
if not token_is_valid(refresh_token):
|
|
110
|
-
raise ClientAuthenticationError("Refresh token has expired")
|
|
111
|
-
|
|
112
|
-
# Update tokens using existing refresh_token
|
|
113
|
-
data = AuthRequest(
|
|
114
|
-
client_id=client_id, grant_type=GrantType.REFRESH, username=None, password=None, refresh_token=refresh_token
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
request_url = slash_join(url, f"realms/{realm}/protocol/openid-connect/token")
|
|
118
|
-
result = requests.post(request_url, data=data.model_dump(exclude_none=True), timeout=AUTH_REQUESTS_TIMEOUT)
|
|
119
|
-
if result.status_code != 200:
|
|
120
|
-
raise ClientAuthenticationError(f"Failed to update tokens, {result.text}")
|
|
121
|
-
tokens = result.json()
|
|
122
|
-
if not tokens or "access_token" not in tokens or "refresh_token" not in tokens:
|
|
123
|
-
raise ClientAuthenticationError("Failed to get new tokens")
|
|
124
|
-
tokens = {key: tokens.get(key, "") for key in ["access_token", "refresh_token"]}
|
|
125
|
-
return tokens
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def logout_request(url: str, realm: str, client_id: str, refresh_token: str) -> bool:
|
|
129
|
-
"""Sends logout request to the authentication server.
|
|
130
|
-
|
|
131
|
-
Raises:
|
|
132
|
-
ClientAuthenticationError: updating the tokens failed
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
True if logout was successful
|
|
136
|
-
|
|
137
|
-
"""
|
|
138
|
-
data = AuthRequest(client_id=client_id, grant_type=None, username=None, password=None, refresh_token=refresh_token)
|
|
139
|
-
request_url = slash_join(url, f"realms/{realm}/protocol/openid-connect/logout")
|
|
140
|
-
result = requests.post(request_url, data=data.model_dump(exclude_none=True), timeout=AUTH_REQUESTS_TIMEOUT)
|
|
141
|
-
|
|
142
|
-
if result.status_code != 204:
|
|
143
|
-
raise ClientAuthenticationError(f"Failed to logout, {result.text}")
|
|
144
|
-
return True
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def time_left_seconds(token: str) -> int:
|
|
148
|
-
"""Check how much time is left until the token expires.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Time left on token in seconds.
|
|
152
|
-
|
|
153
|
-
"""
|
|
154
|
-
_, body, _ = token.split(".", 2)
|
|
155
|
-
# Add padding to adjust body length to a multiple of 4 chars as required by base64 decoding
|
|
156
|
-
body += "=" * (-len(body) % 4)
|
|
157
|
-
exp_time = int(json.loads(b64decode(body)).get("exp", "0"))
|
|
158
|
-
return max(0, exp_time - int(time.time()))
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def token_is_valid(refresh_token: str) -> bool:
|
|
162
|
-
"""Check if token is not about to expire.
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
True if token is still valid, False otherwise.
|
|
166
|
-
|
|
167
|
-
"""
|
|
168
|
-
return time_left_seconds(refresh_token) > REFRESH_MARGIN_SECONDS
|