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.
@@ -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
- """This file is an example of using Qiskit on IQM to run a simple but non-trivial quantum circuit on an IQM quantum
15
- computer. See the Qiskit on IQM user guide for instructions:
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
- """Run a circuit that prepares and measures a Bell state.
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
- a mapping of bitstrings representing qubit measurement results to counts for each result
35
+ Mapping of bitstrings representing qubit measurement results to counts for each result.
33
36
 
34
37
  """
35
- backend = IQMProvider(server_url).get_backend()
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
- new_circuit = transpile(circuit, backend)
44
- return backend.run(new_circuit, shots=1000).result().get_counts()
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
- print(bell_measure(argparser.parse_args().url))
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=[
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-client
3
- Version: 31.7.0
3
+ Version: 32.0.0
4
4
  Summary: Client library for accessing an IQM quantum computer
5
5
  Author-email: IQM Finland Oy <developers@meetiqm.com>
6
6
  License: Apache License
@@ -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=4V-rYUZKFMvMOVsroiKB61voHKX_6t7S847NDCdNbQE,7582
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=hvnJYLOiycaXluWQgsip9CAnyb5l8i4xFYHt2qqUg7A,41831
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=9ZuaAZybnEL9KhVom5P-mClQscyEF6k2dn6wV1QJaBQ,1925
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=g5QQ158vd49_SxzteDip97XMD3d50SJymx_rU7g7lzk,2188
55
- iqm/qiskit_iqm/fake_backends/fake_aphrodite.py,sha256=3oWZwPmtcsYkYpqVr36-C5K7KtyQGk84RCvZpaL_kLg,15473
56
- iqm/qiskit_iqm/fake_backends/fake_apollo.py,sha256=eT2vd3kQBi1rrvxCpePymBCfFK84dpwNpqGV_BkltwQ,6563
57
- iqm/qiskit_iqm/fake_backends/fake_deneb.py,sha256=RzQXmLXmBARDiMKVxk5Aw9fVbc6IYlW0A5jibk9iYD0,3156
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-31.7.0.dist-info/AUTHORS.rst,sha256=qsxeK5A3-B_xK3hNbhFHEIkoHNpo7sdzYyRTs7Bdtm8,795
61
- iqm_client-31.7.0.dist-info/LICENSE.txt,sha256=2DXrmQtVVUV9Fc9RBFJidMiTEaQlG2oAtlC9PMrEwTk,11333
62
- iqm_client-31.7.0.dist-info/METADATA,sha256=fXhE5CAodK27QHf-tWtszTun9iSYKMcQUnkA1K0sCCo,17887
63
- iqm_client-31.7.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
64
- iqm_client-31.7.0.dist-info/entry_points.txt,sha256=Kk2qfRwk8vbIJ7qCAvmaUogfRRn6t92_hBFhe6kqAE4,1317
65
- iqm_client-31.7.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
66
- iqm_client-31.7.0.dist-info/RECORD,,
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,,
@@ -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."""
@@ -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