iqm-station-control-client 11.3.1__py3-none-any.whl → 12.0.1__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/station_control/client/authentication.py +239 -0
- iqm/station_control/client/iqm_server/error.py +0 -30
- iqm/station_control/client/iqm_server/grpc_utils.py +0 -156
- iqm/station_control/client/iqm_server/iqm_server_client.py +0 -489
- iqm/station_control/client/list_models.py +16 -11
- iqm/station_control/client/qon.py +1 -1
- iqm/station_control/client/serializers/run_serializers.py +5 -4
- iqm/station_control/client/serializers/struct_serializer.py +1 -1
- iqm/station_control/client/station_control.py +140 -154
- iqm/station_control/client/utils.py +4 -42
- iqm/station_control/interface/models/__init__.py +21 -2
- iqm/station_control/interface/models/circuit.py +348 -0
- iqm/station_control/interface/models/dynamic_quantum_architecture.py +61 -3
- iqm/station_control/interface/models/jobs.py +41 -12
- iqm/station_control/interface/models/observation_set.py +28 -4
- iqm/station_control/interface/models/run.py +8 -8
- iqm/station_control/interface/models/sweep.py +7 -1
- iqm/station_control/interface/models/type_aliases.py +1 -2
- iqm/station_control/interface/station_control.py +1 -1
- {iqm_station_control_client-11.3.1.dist-info → iqm_station_control_client-12.0.1.dist-info}/METADATA +3 -3
- iqm_station_control_client-12.0.1.dist-info/RECORD +42 -0
- iqm/station_control/client/iqm_server/__init__.py +0 -14
- iqm/station_control/client/iqm_server/proto/__init__.py +0 -43
- iqm/station_control/client/iqm_server/proto/calibration_pb2.py +0 -48
- iqm/station_control/client/iqm_server/proto/calibration_pb2.pyi +0 -45
- iqm/station_control/client/iqm_server/proto/calibration_pb2_grpc.py +0 -152
- iqm/station_control/client/iqm_server/proto/common_pb2.py +0 -43
- iqm/station_control/client/iqm_server/proto/common_pb2.pyi +0 -32
- iqm/station_control/client/iqm_server/proto/common_pb2_grpc.py +0 -17
- iqm/station_control/client/iqm_server/proto/job_pb2.py +0 -57
- iqm/station_control/client/iqm_server/proto/job_pb2.pyi +0 -107
- iqm/station_control/client/iqm_server/proto/job_pb2_grpc.py +0 -436
- iqm/station_control/client/iqm_server/proto/qc_pb2.py +0 -51
- iqm/station_control/client/iqm_server/proto/qc_pb2.pyi +0 -57
- iqm/station_control/client/iqm_server/proto/qc_pb2_grpc.py +0 -163
- iqm/station_control/client/iqm_server/proto/uuid_pb2.py +0 -39
- iqm/station_control/client/iqm_server/proto/uuid_pb2.pyi +0 -26
- iqm/station_control/client/iqm_server/proto/uuid_pb2_grpc.py +0 -17
- iqm/station_control/client/iqm_server/testing/__init__.py +0 -13
- iqm/station_control/client/iqm_server/testing/iqm_server_mock.py +0 -102
- iqm_station_control_client-11.3.1.dist-info/RECORD +0 -59
- {iqm_station_control_client-11.3.1.dist-info → iqm_station_control_client-12.0.1.dist-info}/LICENSE.txt +0 -0
- {iqm_station_control_client-11.3.1.dist-info → iqm_station_control_client-12.0.1.dist-info}/WHEEL +0 -0
- {iqm_station_control_client-11.3.1.dist-info → iqm_station_control_client-12.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Copyright 2024 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
|
+
"""This module contains user authentication related classes and functions required by IQMClient."""
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from base64 import b64decode
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
from typing import Any, TypeAlias
|
|
23
|
+
|
|
24
|
+
from iqm.iqm_server_client.errors import ClientAuthenticationError, ClientConfigurationError
|
|
25
|
+
|
|
26
|
+
REFRESH_MARGIN_SECONDS = 60
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
AuthHeaderCallback: TypeAlias = Callable[[], str]
|
|
30
|
+
"""Function that returns an authorization header containing a bearer token."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TokenManager:
|
|
34
|
+
"""TokenManager manages the access token required for user authentication.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
token: Long-lived IQM token in plain text format.
|
|
38
|
+
tokens_file: Path to a tokens file used for authentication.
|
|
39
|
+
auth_header_callback: Callback function that returns an Authorization header containing a bearer token.
|
|
40
|
+
use_env_vars: Iff True, the first two parameters can also be read from the environment variables
|
|
41
|
+
:envvar:`IQM_TOKEN` and :envvar:`IQM_TOKENS_FILE`, respectively.
|
|
42
|
+
Environment variables can not be mixed with initialisation arguments.
|
|
43
|
+
|
|
44
|
+
At most one auth parameter should be given.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def time_left_seconds(token: Any) -> int:
|
|
50
|
+
"""Check how much time is left until the token expires.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Time left on token in seconds.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
if not token or not isinstance(token, str):
|
|
57
|
+
return 0
|
|
58
|
+
parts = token.split(".", 2)
|
|
59
|
+
if len(parts) != 3:
|
|
60
|
+
return 0
|
|
61
|
+
# Add padding to adjust body length to a multiple of 4 chars as required by base64 decoding
|
|
62
|
+
try:
|
|
63
|
+
body = parts[1] + ("=" * (-len(parts[1]) % 4))
|
|
64
|
+
exp_time = int(json.loads(b64decode(body)).get("exp", "0"))
|
|
65
|
+
return max(0, exp_time - int(time.time()))
|
|
66
|
+
except (UnicodeDecodeError, json.decoder.JSONDecodeError, ValueError, TypeError):
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
token: str | None = None,
|
|
72
|
+
tokens_file: str | None = None,
|
|
73
|
+
auth_header_callback: AuthHeaderCallback | None = None,
|
|
74
|
+
*,
|
|
75
|
+
use_env_vars: bool = True,
|
|
76
|
+
):
|
|
77
|
+
def _format_names(variable_names: list[str]) -> str:
|
|
78
|
+
"""Format a list of variable names"""
|
|
79
|
+
return ", ".join(f'"{name}"' for name in variable_names)
|
|
80
|
+
|
|
81
|
+
auth_parameters: dict[str, str] = {}
|
|
82
|
+
|
|
83
|
+
init_parameters = {"token": token, "tokens_file": tokens_file}
|
|
84
|
+
init_params_given = [key for key, value in init_parameters.items() if value]
|
|
85
|
+
if auth_header_callback:
|
|
86
|
+
init_params_given.append("auth_header_callback")
|
|
87
|
+
|
|
88
|
+
if use_env_vars:
|
|
89
|
+
env_variables = {"token": "IQM_TOKEN", "tokens_file": "IQM_TOKENS_FILE"}
|
|
90
|
+
env_vars_given = [name for name in env_variables.values() if os.environ.get(name)]
|
|
91
|
+
else:
|
|
92
|
+
env_variables = {}
|
|
93
|
+
env_vars_given = []
|
|
94
|
+
|
|
95
|
+
if init_params_given and env_vars_given:
|
|
96
|
+
raise ClientConfigurationError(
|
|
97
|
+
"Authentication parameters given both as initialisation args and as environment variables: "
|
|
98
|
+
+ f"initialisation args {_format_names(init_params_given)}, "
|
|
99
|
+
+ f"environment variables {_format_names(env_vars_given)}."
|
|
100
|
+
+ " Parameter sources must not be mixed."
|
|
101
|
+
)
|
|
102
|
+
auth_params_given = init_params_given + env_vars_given
|
|
103
|
+
if len(auth_params_given) > 1:
|
|
104
|
+
raise ClientConfigurationError(
|
|
105
|
+
f"No more than one authentication parameter may be given, received {auth_params_given}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self._token_provider: TokenProviderInterface | None = None
|
|
109
|
+
self._auth_header_callback: AuthHeaderCallback | None = None
|
|
110
|
+
self._access_token: str | None = None
|
|
111
|
+
|
|
112
|
+
if auth_header_callback:
|
|
113
|
+
self._auth_header_callback = auth_header_callback
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if env_vars_given:
|
|
117
|
+
auth_parameters = {key: value for key, name in env_variables.items() if (value := os.environ.get(name))}
|
|
118
|
+
else:
|
|
119
|
+
auth_parameters = {key: value for key, value in init_parameters.items() if value}
|
|
120
|
+
|
|
121
|
+
if not auth_parameters:
|
|
122
|
+
return # no authentication
|
|
123
|
+
if set(auth_parameters) == {"token"}:
|
|
124
|
+
# This is not necessarily a JWT token
|
|
125
|
+
self._token_provider = ExternalToken(auth_parameters["token"])
|
|
126
|
+
elif set(auth_parameters) == {"tokens_file"}:
|
|
127
|
+
self._token_provider = TokensFileReader(auth_parameters["tokens_file"])
|
|
128
|
+
else:
|
|
129
|
+
raise ClientConfigurationError("Not possible.")
|
|
130
|
+
|
|
131
|
+
def get_auth_header_callback(self) -> AuthHeaderCallback | None:
|
|
132
|
+
"""Return a callback providing an Authorization header, or None if we cannot provide it."""
|
|
133
|
+
return self._get_bearer_token if self._token_provider else self._auth_header_callback
|
|
134
|
+
|
|
135
|
+
def _get_bearer_token(self, retries: int = 1) -> str:
|
|
136
|
+
"""Return a valid bearer token.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ClientAuthenticationError: getting the token failed
|
|
140
|
+
RuntimeError: There is no token provider (authentication disabled)
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
if self._token_provider is None:
|
|
144
|
+
raise RuntimeError("There is no token provider.")
|
|
145
|
+
|
|
146
|
+
# Use the existing access token if it is still valid
|
|
147
|
+
if TokenManager.time_left_seconds(self._access_token) > REFRESH_MARGIN_SECONDS:
|
|
148
|
+
return f"Bearer {self._access_token}"
|
|
149
|
+
|
|
150
|
+
# Otherwise, get a new access token from token provider
|
|
151
|
+
try:
|
|
152
|
+
self._access_token = self._token_provider.get_token()
|
|
153
|
+
return f"Bearer {self._access_token}"
|
|
154
|
+
except ClientAuthenticationError:
|
|
155
|
+
if retries < 1:
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
# Try again
|
|
159
|
+
return self._get_bearer_token(retries - 1)
|
|
160
|
+
|
|
161
|
+
def close(self) -> bool:
|
|
162
|
+
"""Close the configured token provider.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if closing was successful
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ClientAuthenticationError: closing failed
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
if self._token_provider is None:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
self._token_provider.close()
|
|
175
|
+
self._token_provider = None
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TokenProviderInterface(ABC):
|
|
180
|
+
"""Interface to token provider"""
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def get_token(self) -> str:
|
|
184
|
+
"""Return a valid access token.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ClientAuthenticationError: acquiring the token failed
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def close(self) -> None:
|
|
193
|
+
"""Close the authentication session.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ClientAuthenticationError: closing the session failed
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ExternalToken(TokenProviderInterface):
|
|
202
|
+
"""Holds an external token"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, token: str):
|
|
205
|
+
self._token: str | None = token
|
|
206
|
+
|
|
207
|
+
def get_token(self) -> str:
|
|
208
|
+
if self._token is None:
|
|
209
|
+
raise ClientAuthenticationError("No external token available")
|
|
210
|
+
return self._token
|
|
211
|
+
|
|
212
|
+
def close(self) -> None:
|
|
213
|
+
self._token = None
|
|
214
|
+
raise ClientAuthenticationError("Can not close externally managed auth session")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TokensFileReader(TokenProviderInterface):
|
|
218
|
+
"""Reads token from a file"""
|
|
219
|
+
|
|
220
|
+
def __init__(self, tokens_file: str):
|
|
221
|
+
self._path: str | None = tokens_file
|
|
222
|
+
|
|
223
|
+
def get_token(self) -> str:
|
|
224
|
+
try:
|
|
225
|
+
if self._path is None:
|
|
226
|
+
raise ClientAuthenticationError("No tokens file available")
|
|
227
|
+
with open(self._path, encoding="utf-8") as file:
|
|
228
|
+
raw_data = file.read()
|
|
229
|
+
json_data = json.loads(raw_data)
|
|
230
|
+
token = json_data.get("access_token")
|
|
231
|
+
if TokenManager.time_left_seconds(token) <= 0:
|
|
232
|
+
raise ClientAuthenticationError("Access token in file has expired or is not valid")
|
|
233
|
+
except (FileNotFoundError, IsADirectoryError, json.decoder.JSONDecodeError) as e:
|
|
234
|
+
raise ClientAuthenticationError(rf"Failed to read access token from file '{self._path}': {e}") from e
|
|
235
|
+
return token
|
|
236
|
+
|
|
237
|
+
def close(self) -> None:
|
|
238
|
+
self._path = None
|
|
239
|
+
raise ClientAuthenticationError("Can not close externally managed auth session")
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Copyright 2025 IQM
|
|
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
|
-
|
|
15
|
-
from exa.common.errors.station_control_errors import StationControlError
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class IqmServerError(StationControlError):
|
|
19
|
-
def __init__(self, message: str, status_code: str, error_code: str | None = None, details: dict | None = None):
|
|
20
|
-
super().__init__(message)
|
|
21
|
-
self.status_code = status_code
|
|
22
|
-
self.error_code = error_code
|
|
23
|
-
self.details = details
|
|
24
|
-
|
|
25
|
-
def __str__(self):
|
|
26
|
-
s = f"{self.message} (status_code = {self.status_code}, error_code = {self.error_code}"
|
|
27
|
-
if details := self.details:
|
|
28
|
-
s += f", details = {details}"
|
|
29
|
-
s += ")"
|
|
30
|
-
return s
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
# Copyright 2025 IQM
|
|
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
|
-
"""Internal utility functions used by IqmServerClient."""
|
|
15
|
-
|
|
16
|
-
from collections.abc import Callable, Iterable
|
|
17
|
-
from dataclasses import dataclass
|
|
18
|
-
from datetime import datetime
|
|
19
|
-
import uuid
|
|
20
|
-
|
|
21
|
-
from google.protobuf import json_format, struct_pb2, timestamp_pb2
|
|
22
|
-
import grpc
|
|
23
|
-
from grpc import Compression
|
|
24
|
-
from pydantic import HttpUrl
|
|
25
|
-
|
|
26
|
-
from iqm.station_control.client.iqm_server import proto
|
|
27
|
-
from iqm.station_control.client.iqm_server.error import IqmServerError
|
|
28
|
-
from iqm.station_control.interface.models.type_aliases import StrUUID
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class ClientCallDetails(grpc.ClientCallDetails):
|
|
32
|
-
def __init__(self, details): # noqa: ANN001
|
|
33
|
-
self.method = details.method
|
|
34
|
-
self.metadata = list(details.metadata or [])
|
|
35
|
-
self.timeout = details.timeout
|
|
36
|
-
self.credentials = details.credentials
|
|
37
|
-
self.wait_for_ready = details.wait_for_ready
|
|
38
|
-
self.compression = details.compression
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ApiTokenAuth(grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor):
|
|
42
|
-
def __init__(self, get_token_callback: Callable[[], str]):
|
|
43
|
-
self.get_token_callback = get_token_callback
|
|
44
|
-
|
|
45
|
-
def _add_auth_header(self, client_call_details) -> ClientCallDetails: # noqa: ANN001
|
|
46
|
-
details = ClientCallDetails(client_call_details)
|
|
47
|
-
details.metadata.append(("authorization", self.get_token_callback()))
|
|
48
|
-
return details
|
|
49
|
-
|
|
50
|
-
def intercept_unary_stream(self, continuation, client_call_details, request): # noqa: ANN001, ANN201
|
|
51
|
-
return continuation(self._add_auth_header(client_call_details), request)
|
|
52
|
-
|
|
53
|
-
def intercept_unary_unary(self, continuation, client_call_details, request): # noqa: ANN001, ANN201
|
|
54
|
-
return continuation(self._add_auth_header(client_call_details), request)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass(frozen=True, kw_only=True)
|
|
58
|
-
class ConnectionParameters:
|
|
59
|
-
server_address: str
|
|
60
|
-
is_secure: bool
|
|
61
|
-
quantum_computer: str
|
|
62
|
-
use_timeslot: bool
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def parse_connection_params(qc_url: str) -> ConnectionParameters:
|
|
66
|
-
# Security measure: mitigate UTF-8 read order control character
|
|
67
|
-
# exploits by allowing only ASCII urls
|
|
68
|
-
if not qc_url.isascii():
|
|
69
|
-
raise ValueError("Invalid quantum computer URL")
|
|
70
|
-
|
|
71
|
-
# IQM Server QC urls are now form "https://cocos.<server_base_url>/<qc_name>[:timeslot]"
|
|
72
|
-
# In the future, "cocos." subdomain will be dropped. The parsing logic should work with
|
|
73
|
-
# the both url formats
|
|
74
|
-
url = HttpUrl(qc_url)
|
|
75
|
-
qc_name = (url.path or "").split("/")[-1].removesuffix(":timeslot")
|
|
76
|
-
use_timeslot = qc_url.endswith(":timeslot")
|
|
77
|
-
if not qc_name:
|
|
78
|
-
raise ValueError("Invalid quantum computer URL: device name is missing")
|
|
79
|
-
|
|
80
|
-
is_secure = url.scheme == "https"
|
|
81
|
-
hostname = (url.host or "").removeprefix("cocos.")
|
|
82
|
-
port = url.port or (443 if is_secure else 80)
|
|
83
|
-
|
|
84
|
-
return ConnectionParameters(
|
|
85
|
-
server_address=f"{hostname}:{port}",
|
|
86
|
-
is_secure=is_secure,
|
|
87
|
-
quantum_computer=qc_name,
|
|
88
|
-
use_timeslot=use_timeslot,
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def create_channel(
|
|
93
|
-
connection_params: ConnectionParameters,
|
|
94
|
-
get_token_callback: Callable[[], str] | None = None,
|
|
95
|
-
enable_compression: bool = True,
|
|
96
|
-
) -> grpc.Channel:
|
|
97
|
-
compression = Compression.Gzip if enable_compression else None
|
|
98
|
-
options = [
|
|
99
|
-
# Let's try to parametrize this at least when we're merging station-control-client and iqm-client
|
|
100
|
-
("grpc.keepalive_time_ms", 5000),
|
|
101
|
-
("grpc.keepalive_permit_without_calls", 1),
|
|
102
|
-
("grpc.http2.max_pings_without_data", 0),
|
|
103
|
-
("grpc.keepalive_timeout_ms", 1000),
|
|
104
|
-
]
|
|
105
|
-
address = connection_params.server_address
|
|
106
|
-
channel = (
|
|
107
|
-
grpc.secure_channel(
|
|
108
|
-
address, credentials=grpc.ssl_channel_credentials(), options=options, compression=compression
|
|
109
|
-
)
|
|
110
|
-
if connection_params.is_secure
|
|
111
|
-
else grpc.insecure_channel(address, options=options, compression=compression)
|
|
112
|
-
)
|
|
113
|
-
if get_token_callback is not None:
|
|
114
|
-
channel = grpc.intercept_channel(channel, ApiTokenAuth(get_token_callback))
|
|
115
|
-
return channel
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def to_proto_uuid(value: StrUUID) -> proto.Uuid:
|
|
119
|
-
if isinstance(value, str):
|
|
120
|
-
value = uuid.UUID(value)
|
|
121
|
-
return proto.Uuid(raw=value.bytes)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def from_proto_uuid(value: proto.Uuid) -> uuid.UUID:
|
|
125
|
-
if value.WhichOneof("data") == "str":
|
|
126
|
-
return uuid.UUID(hex=value.str)
|
|
127
|
-
return uuid.UUID(bytes=value.raw)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def to_datetime(timestamp: timestamp_pb2.Timestamp) -> datetime:
|
|
131
|
-
return timestamp.ToDatetime()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def load_all(chunks: Iterable[proto.DataChunk]) -> bytes:
|
|
135
|
-
result = bytearray()
|
|
136
|
-
for chunk in chunks:
|
|
137
|
-
result.extend(chunk.data)
|
|
138
|
-
return bytes(result)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def extract_error(error: grpc.RpcError, title: str | None = None) -> IqmServerError:
|
|
142
|
-
message = error.details()
|
|
143
|
-
status_code = str(error.code().name)
|
|
144
|
-
metadata = {k: v for k, v in list(error.initial_metadata()) + list(error.trailing_metadata())}
|
|
145
|
-
error_code = str(metadata.get("error_code")) if "error_code" in metadata else None
|
|
146
|
-
details = None
|
|
147
|
-
if details_bin := metadata.get("grpc-status-details-bin"):
|
|
148
|
-
value_proto = struct_pb2.Value()
|
|
149
|
-
value_proto.ParseFromString(details_bin)
|
|
150
|
-
details = json_format.MessageToJson(value_proto)
|
|
151
|
-
return IqmServerError(
|
|
152
|
-
message=f"{title}: {message}" if title else message,
|
|
153
|
-
status_code=status_code,
|
|
154
|
-
error_code=error_code,
|
|
155
|
-
details=details, # type: ignore[arg-type]
|
|
156
|
-
)
|