iqm-station-control-client 8.0.0__py3-none-any.whl → 9.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/station_control/client/__init__.py +1 -1
- iqm/station_control/client/iqm_server/error.py +1 -1
- iqm/station_control/client/iqm_server/grpc_utils.py +5 -3
- iqm/station_control/client/iqm_server/iqm_server_client.py +276 -45
- iqm/station_control/client/list_models.py +18 -12
- iqm/station_control/client/serializers/__init__.py +1 -1
- iqm/station_control/client/serializers/channel_property_serializer.py +12 -6
- iqm/station_control/client/serializers/datetime_serializers.py +1 -1
- iqm/station_control/client/serializers/playlist_serializers.py +1 -1
- iqm/station_control/client/serializers/run_serializers.py +1 -1
- iqm/station_control/client/serializers/setting_node_serializer.py +1 -1
- iqm/station_control/client/serializers/struct_serializer.py +1 -1
- iqm/station_control/client/serializers/sweep_serializers.py +2 -3
- iqm/station_control/client/serializers/task_serializers.py +1 -1
- iqm/station_control/client/station_control.py +162 -443
- iqm/station_control/client/utils.py +44 -17
- iqm/station_control/interface/__init__.py +1 -1
- iqm/station_control/interface/list_with_meta.py +1 -1
- iqm/station_control/interface/models/__init__.py +13 -1
- iqm/station_control/interface/models/dut.py +1 -1
- iqm/station_control/interface/models/dynamic_quantum_architecture.py +98 -0
- iqm/station_control/interface/models/jobs.py +27 -3
- iqm/station_control/interface/models/observation.py +1 -1
- iqm/station_control/interface/models/observation_set.py +15 -1
- iqm/station_control/interface/models/run.py +1 -1
- iqm/station_control/interface/models/sequence.py +1 -1
- iqm/station_control/interface/models/static_quantum_architecture.py +40 -0
- iqm/station_control/interface/models/sweep.py +1 -1
- iqm/station_control/interface/models/type_aliases.py +7 -1
- iqm/station_control/interface/pydantic_base.py +1 -1
- iqm/station_control/interface/station_control.py +511 -0
- {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/METADATA +2 -2
- iqm_station_control_client-9.0.0.dist-info/RECORD +56 -0
- iqm/station_control/client/iqm_server/meta_class.py +0 -38
- iqm_station_control_client-8.0.0.dist-info/RECORD +0 -54
- {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/LICENSE.txt +0 -0
- {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/WHEEL +0 -0
- {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
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
|
-
"""
|
|
14
|
+
"""Client implementation for station control service REST API."""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
@@ -21,6 +21,7 @@ from importlib.metadata import version
|
|
|
21
21
|
import json
|
|
22
22
|
import logging
|
|
23
23
|
import os
|
|
24
|
+
import platform
|
|
24
25
|
from time import sleep
|
|
25
26
|
from typing import Any, TypeVar
|
|
26
27
|
import uuid
|
|
@@ -32,8 +33,7 @@ from pydantic import BaseModel
|
|
|
32
33
|
import requests
|
|
33
34
|
|
|
34
35
|
from exa.common.data.setting_node import SettingNode
|
|
35
|
-
from exa.common.
|
|
36
|
-
from exa.common.errors.server_errors import (
|
|
36
|
+
from exa.common.errors.station_control_errors import (
|
|
37
37
|
InternalServerError,
|
|
38
38
|
NotFoundError,
|
|
39
39
|
StationControlError,
|
|
@@ -62,12 +62,14 @@ from iqm.station_control.client.serializers import (
|
|
|
62
62
|
from iqm.station_control.client.serializers.channel_property_serializer import unpack_channel_properties
|
|
63
63
|
from iqm.station_control.client.serializers.setting_node_serializer import deserialize_setting_node
|
|
64
64
|
from iqm.station_control.client.serializers.sweep_serializers import deserialize_sweep_data
|
|
65
|
-
from iqm.station_control.client.utils import calset_from_observations
|
|
66
65
|
from iqm.station_control.interface.list_with_meta import ListWithMeta
|
|
67
66
|
from iqm.station_control.interface.models import (
|
|
68
67
|
DutData,
|
|
69
68
|
DutFieldData,
|
|
69
|
+
DynamicQuantumArchitecture,
|
|
70
70
|
GetObservationsMode,
|
|
71
|
+
JobData,
|
|
72
|
+
JobExecutorStatus,
|
|
71
73
|
ObservationData,
|
|
72
74
|
ObservationDefinition,
|
|
73
75
|
ObservationLite,
|
|
@@ -75,6 +77,7 @@ from iqm.station_control.interface.models import (
|
|
|
75
77
|
ObservationSetDefinition,
|
|
76
78
|
ObservationSetUpdate,
|
|
77
79
|
ObservationUpdate,
|
|
80
|
+
QualityMetrics,
|
|
78
81
|
RunData,
|
|
79
82
|
RunDefinition,
|
|
80
83
|
RunLite,
|
|
@@ -83,150 +86,73 @@ from iqm.station_control.interface.models import (
|
|
|
83
86
|
SequenceResultData,
|
|
84
87
|
SequenceResultDefinition,
|
|
85
88
|
SoftwareVersionSet,
|
|
89
|
+
StaticQuantumArchitecture,
|
|
86
90
|
Statuses,
|
|
87
91
|
SweepData,
|
|
88
92
|
SweepDefinition,
|
|
89
93
|
SweepResults,
|
|
90
94
|
)
|
|
91
|
-
from iqm.station_control.interface.models.
|
|
95
|
+
from iqm.station_control.interface.models.type_aliases import StrUUID
|
|
92
96
|
from iqm.station_control.interface.pydantic_base import PydanticBase
|
|
97
|
+
from iqm.station_control.interface.station_control import StationControlInterface
|
|
93
98
|
|
|
94
99
|
logger = logging.getLogger(__name__)
|
|
95
|
-
|
|
100
|
+
TypePydanticBase = TypeVar("TypePydanticBase", bound=PydanticBase)
|
|
96
101
|
|
|
97
102
|
|
|
98
|
-
class StationControlClient:
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
Current implementation uses HTTP calls to the remote station control service,
|
|
102
|
-
that is controlling the station control instance.
|
|
103
|
+
class StationControlClient(StationControlInterface):
|
|
104
|
+
"""Client implementation for station control service REST API.
|
|
103
105
|
|
|
104
106
|
Args:
|
|
105
107
|
root_url: Remote station control service URL.
|
|
106
|
-
get_token_callback: A callback function that returns a token (str)
|
|
107
|
-
in all requests.
|
|
108
|
-
|
|
109
|
-
Station control client implements generic query methods for certain objects,
|
|
110
|
-
like :meth:`query_observations`, :meth:`query_observation_sets`, and :meth:`query_sequence_metadatas`.
|
|
111
|
-
These methods accept only keyword arguments as parameters, which are based on the syntax ``field__lookup=value``.
|
|
112
|
-
Note double-underscore in the name, to separate field names like ``dut_field`` from lookup types like ``in``.
|
|
113
|
-
The syntax is based on Django implementation, documented
|
|
114
|
-
`here <https://docs.djangoproject.com/en/5.0/ref/models/querysets/#field-lookups>`__ and
|
|
115
|
-
`here <https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/fields/#querying-arrayfield>`__.
|
|
116
|
-
|
|
117
|
-
As a convenience, when no lookup type is provided (like in ``dut_label="foo"``),
|
|
118
|
-
the lookup type is assumed to be exact (``dut_label__exact="foo"``). Other supported lookup types are:
|
|
119
|
-
|
|
120
|
-
- range: Range test (inclusive).
|
|
121
|
-
For example, ``created_timestamp__range=(datetime(2023, 10, 12), datetime(2024, 10, 14))``
|
|
122
|
-
- in: In a given iterable; often a list, tuple, or queryset.
|
|
123
|
-
For example, ``dut_field__in=["QB1.frequency", "gates.measure.constant.QB2.frequency"]``
|
|
124
|
-
- icontains: Case-insensitive containment test.
|
|
125
|
-
For example, ``origin_uri__icontains="local"``
|
|
126
|
-
- overlap: Returns objects where the data shares any results with the values passed.
|
|
127
|
-
For example, ``tags__overlap=["calibration=good", "2023-12-04"]``
|
|
128
|
-
- contains: The returned objects will be those where the values passed are a subset of the data.
|
|
129
|
-
For example, ``tags__contains=["calibration=good", "2023-12-04"]``
|
|
130
|
-
- isnull: Takes either True or False, which correspond to SQL queries of IS NULL and IS NOT NULL, respectively.
|
|
131
|
-
For example, ``end_timestamp__isnull=False``
|
|
132
|
-
|
|
133
|
-
In addition to model fields (like "dut_label", "dut_field", "created_timestamp", "invalid", etc.),
|
|
134
|
-
all of our generic query methods accept also following shared query parameters:
|
|
135
|
-
|
|
136
|
-
- latest: str. Return only the latest item for this field, based on "created_timestamp".
|
|
137
|
-
For example, ``latest="invalid"`` would return only one result (latest "created_timestamp")
|
|
138
|
-
for each different "invalid" value in the database. Thus, maximum three results would be returned,
|
|
139
|
-
one for each invalid value of `True`, `False`, and `None`.
|
|
140
|
-
- order_by: str. Prefix with "-" for descending order, for example "-created_timestamp".
|
|
141
|
-
- limit: int: Default 20. If 0 (or negative number) is given, then pagination is not used, i.e. limit=infinity.
|
|
142
|
-
- offset: int. Default 0.
|
|
143
|
-
|
|
144
|
-
Our generic query methods are not fully generalized yet, thus not all fields and lookup types are supported.
|
|
145
|
-
Check query methods own documentation for details about currently supported query parameters.
|
|
146
|
-
|
|
147
|
-
Generic query methods will return a list of objects, but with additional (optional) "meta" attribute,
|
|
148
|
-
which contains metadata, like pagination details. The client can ignore this data,
|
|
149
|
-
or use it to implement pagination logic for example to fetch all results available.
|
|
108
|
+
get_token_callback: A callback function that returns a token (str)
|
|
109
|
+
which will be passed in Authorization header in all requests.
|
|
150
110
|
|
|
151
111
|
"""
|
|
152
112
|
|
|
153
|
-
def __init__(
|
|
113
|
+
def __init__(
|
|
114
|
+
self, root_url: str, *, get_token_callback: Callable[[], str] | None = None, client_signature: str | None = None
|
|
115
|
+
):
|
|
154
116
|
self.root_url = root_url
|
|
155
117
|
self._enable_opentelemetry = os.environ.get("JAEGER_OPENTELEMETRY_COLLECTOR_ENDPOINT", None) is not None
|
|
156
118
|
self._get_token_callback = get_token_callback
|
|
119
|
+
self._signature = self._create_signature(client_signature)
|
|
157
120
|
# TODO SW-1387: Remove this when using v1 API, not needed
|
|
158
121
|
self._check_api_versions()
|
|
159
|
-
|
|
160
|
-
self._qcm_data_client = QCMDataClient(
|
|
122
|
+
qcm_data_url = os.environ.get("CHIP_DESIGN_RECORD_FALLBACK_URL", None)
|
|
123
|
+
self._qcm_data_client = QCMDataClient(qcm_data_url) if qcm_data_url else None
|
|
161
124
|
|
|
162
125
|
@property
|
|
163
126
|
def version(self) -> str:
|
|
164
127
|
"""Return the version of the station control API this client is using."""
|
|
165
128
|
return "v1"
|
|
166
129
|
|
|
167
|
-
@staticmethod
|
|
168
|
-
def init(root_url: str, get_token_callback: Callable[[], str] | None = None, **kwargs) -> StationControlClient:
|
|
169
|
-
"""Initialize a new station control client instance connected to the given remote.
|
|
170
|
-
|
|
171
|
-
Client implementation is selected automatically based on the remote station: if the remote station
|
|
172
|
-
is running the IQM Server software stack, then the IQM Server client implementation (with a limited
|
|
173
|
-
feature set) is chosen. If the remote station is running the SC software stack, then the Station
|
|
174
|
-
Control client implementation (with the full feature set) is chosen.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
root_url: Remote station control service URL. For IQM Server remotes, this is the "Quantum Computer URL"
|
|
178
|
-
value from the web dashboard.
|
|
179
|
-
get_token_callback: A callback function that returns a token (str) which will be passed in Authorization
|
|
180
|
-
header in all requests.
|
|
181
|
-
|
|
182
|
-
"""
|
|
183
|
-
try:
|
|
184
|
-
headers = {"Authorization": f"Bearer {get_token_callback()}"} if get_token_callback else {}
|
|
185
|
-
response = requests.get(f"{root_url}/about", headers=headers)
|
|
186
|
-
response.raise_for_status()
|
|
187
|
-
about = response.json()
|
|
188
|
-
if isinstance(about, dict) and about.get("iqm_server") is True:
|
|
189
|
-
# If about information has iqm_server flag, it means that we're communicating
|
|
190
|
-
# with IQM server instead of direct Station Control service, hence we need to
|
|
191
|
-
# use the specialized client
|
|
192
|
-
|
|
193
|
-
# Must be imported here in order to avoid circular dependencies
|
|
194
|
-
from iqm.station_control.client.iqm_server.iqm_server_client import IqmServerClient
|
|
195
|
-
|
|
196
|
-
return IqmServerClient(root_url, get_token_callback, **kwargs)
|
|
197
|
-
# Using direct station control by default
|
|
198
|
-
return StationControlClient(root_url, get_token_callback)
|
|
199
|
-
|
|
200
|
-
except Exception as e:
|
|
201
|
-
raise StationControlError("Failed to connect to the remote server") from e
|
|
202
|
-
|
|
203
130
|
@cache
|
|
204
131
|
def get_about(self) -> dict:
|
|
205
|
-
"""Return information about the station control."""
|
|
206
132
|
response = self._send_request(requests.get, "about")
|
|
207
133
|
return response.json()
|
|
208
134
|
|
|
135
|
+
def get_health(self) -> dict:
|
|
136
|
+
response = self._send_request(requests.get, "health")
|
|
137
|
+
return response.json()
|
|
138
|
+
|
|
209
139
|
@cache
|
|
210
140
|
def get_configuration(self) -> dict:
|
|
211
|
-
"""Return the configuration of the station control."""
|
|
212
141
|
response = self._send_request(requests.get, "configuration")
|
|
213
142
|
return response.json()
|
|
214
143
|
|
|
215
144
|
@cache
|
|
216
145
|
def get_exa_configuration(self) -> str:
|
|
217
|
-
"""Return the recommended EXA configuration from the server."""
|
|
218
146
|
response = self._send_request(requests.get, "exa/configuration")
|
|
219
147
|
return response.content.decode("utf-8")
|
|
220
148
|
|
|
221
149
|
def get_or_create_software_version_set(self, software_version_set: SoftwareVersionSet) -> int:
|
|
222
|
-
"""Get software version set ID from the database, or create if it doesn't exist."""
|
|
223
150
|
# FIXME: We don't have information if the object was created or fetched. Thus, server always responds 200 (OK).
|
|
224
151
|
json_str = json.dumps(software_version_set)
|
|
225
152
|
response = self._send_request(requests.post, "software-version-sets", json_str=json_str)
|
|
226
153
|
return int(response.content)
|
|
227
154
|
|
|
228
155
|
def get_settings(self) -> SettingNode:
|
|
229
|
-
"""Return a tree representation of the default settings as defined in the configuration file."""
|
|
230
156
|
return self._get_cached_settings().model_copy()
|
|
231
157
|
|
|
232
158
|
@cache
|
|
@@ -236,7 +162,6 @@ class StationControlClient:
|
|
|
236
162
|
|
|
237
163
|
@cache
|
|
238
164
|
def get_chip_design_record(self, dut_label: str) -> dict:
|
|
239
|
-
"""Get a raw chip design record matching the given chip label."""
|
|
240
165
|
try:
|
|
241
166
|
response = self._send_request(requests.get, f"chip-design-records/{dut_label}")
|
|
242
167
|
except StationControlError as err:
|
|
@@ -247,17 +172,8 @@ class StationControlClient:
|
|
|
247
172
|
|
|
248
173
|
@cache
|
|
249
174
|
def get_channel_properties(self) -> dict[str, ChannelProperties]:
|
|
250
|
-
"""Get channel properties from the station.
|
|
251
|
-
|
|
252
|
-
Channel properties contain information regarding hardware limitations e.g. sampling rate, granularity
|
|
253
|
-
and supported instructions.
|
|
254
|
-
|
|
255
|
-
Returns:
|
|
256
|
-
Mapping from channel name to AWGProperties or ReadoutProperties
|
|
257
|
-
|
|
258
|
-
"""
|
|
259
175
|
headers = {"accept": "application/octet-stream"}
|
|
260
|
-
response = self._send_request(requests.get, "channel-properties
|
|
176
|
+
response = self._send_request(requests.get, "channel-properties", headers=headers)
|
|
261
177
|
decoded_dict = unpack_channel_properties(response.content)
|
|
262
178
|
return decoded_dict
|
|
263
179
|
|
|
@@ -265,49 +181,17 @@ class StationControlClient:
|
|
|
265
181
|
self,
|
|
266
182
|
sweep_definition: SweepDefinition,
|
|
267
183
|
) -> dict:
|
|
268
|
-
"""Execute an N-dimensional sweep of selected variables and save sweep and results.
|
|
269
|
-
|
|
270
|
-
The raw data for each spot in the sweep is saved as numpy arrays,
|
|
271
|
-
and the complete data for the whole sweep is saved as an x-array dataset
|
|
272
|
-
which has the `sweep_definition.sweeps` as coordinates and
|
|
273
|
-
data of `sweep_definition.return_parameters` data as DataArrays.
|
|
274
|
-
|
|
275
|
-
The values of `sweep_definition.playlist` will be uploaded to the controllers given by the keys of
|
|
276
|
-
`sweep_definition.playlist`.
|
|
277
|
-
|
|
278
|
-
Args:
|
|
279
|
-
sweep_definition: The content of the sweep to be created.
|
|
280
|
-
|
|
281
|
-
Returns:
|
|
282
|
-
Dict containing the job ID and sweep ID, and corresponding hrefs, of a successful sweep execution
|
|
283
|
-
in monolithic mode or successful submission to the job queue in remote mode.
|
|
284
|
-
|
|
285
|
-
Raises:
|
|
286
|
-
ExaError if submitting a sweep failed.
|
|
287
|
-
|
|
288
|
-
"""
|
|
289
184
|
data = serialize_sweep_job_request(sweep_definition, queue_name="sweeps")
|
|
290
185
|
return self._send_request(requests.post, "sweeps", octets=data).json()
|
|
291
186
|
|
|
292
|
-
def get_sweep(self, sweep_id:
|
|
293
|
-
"""Get N-dimensional sweep data from the database."""
|
|
187
|
+
def get_sweep(self, sweep_id: StrUUID) -> SweepData:
|
|
294
188
|
response = self._send_request(requests.get, f"sweeps/{sweep_id}")
|
|
295
189
|
return deserialize_sweep_data(response.json())
|
|
296
190
|
|
|
297
|
-
def
|
|
298
|
-
"""Either remove a job from the queue, or abort it gracefully if it's already executing.
|
|
299
|
-
|
|
300
|
-
The status of the job will be set to ``JobStatus.ABORTED``.
|
|
301
|
-
If the job is not found or is already finished nothing happens.
|
|
302
|
-
"""
|
|
303
|
-
self._send_request(requests.post, f"jobs/{job_id}/abort")
|
|
304
|
-
|
|
305
|
-
def delete_sweep(self, sweep_id: uuid.UUID) -> None:
|
|
306
|
-
"""Delete sweep in the database."""
|
|
191
|
+
def delete_sweep(self, sweep_id: StrUUID) -> None:
|
|
307
192
|
self._send_request(requests.delete, f"sweeps/{sweep_id}")
|
|
308
193
|
|
|
309
|
-
def get_sweep_results(self, sweep_id:
|
|
310
|
-
"""Get N-dimensional sweep results from the database."""
|
|
194
|
+
def get_sweep_results(self, sweep_id: StrUUID) -> SweepResults:
|
|
311
195
|
response = self._send_request(requests.get, f"sweeps/{sweep_id}/results")
|
|
312
196
|
return deserialize_sweep_results(response.content)
|
|
313
197
|
|
|
@@ -317,7 +201,6 @@ class StationControlClient:
|
|
|
317
201
|
update_progress_callback: Callable[[Statuses], None] | None = None,
|
|
318
202
|
wait_job_completion: bool = True,
|
|
319
203
|
) -> bool:
|
|
320
|
-
"""Execute an N-dimensional sweep of selected variables and save run, sweep and results."""
|
|
321
204
|
data = serialize_run_job_request(run_definition, queue_name="sweeps")
|
|
322
205
|
|
|
323
206
|
response = self._send_request(requests.post, "runs", octets=data)
|
|
@@ -325,61 +208,21 @@ class StationControlClient:
|
|
|
325
208
|
return self._wait_job_completion(response.json()["job_id"], update_progress_callback)
|
|
326
209
|
return False
|
|
327
210
|
|
|
328
|
-
def get_run(self, run_id:
|
|
329
|
-
"""Get run data from the database."""
|
|
211
|
+
def get_run(self, run_id: StrUUID) -> RunData:
|
|
330
212
|
response = self._send_request(requests.get, f"runs/{run_id}")
|
|
331
213
|
return deserialize_run_data(response.json())
|
|
332
214
|
|
|
333
215
|
def query_runs(self, **kwargs) -> ListWithMeta[RunLite]:
|
|
334
|
-
"""Query runs from the database.
|
|
335
|
-
|
|
336
|
-
Runs are queried by the given query parameters. Currently supported query parameters:
|
|
337
|
-
- run_id: uuid.UUID
|
|
338
|
-
- run_id__in: list[uuid.UUID]
|
|
339
|
-
- sweep_id: uuid.UUID
|
|
340
|
-
- sweep_id__in: list[uuid.UUID]
|
|
341
|
-
- username: str
|
|
342
|
-
- username__in: list[str]
|
|
343
|
-
- username__contains: str
|
|
344
|
-
- username__icontains: str
|
|
345
|
-
- experiment_label: str
|
|
346
|
-
- experiment_label__in: list[str]
|
|
347
|
-
- experiment_label__contains: str
|
|
348
|
-
- experiment_label__icontains: str
|
|
349
|
-
- experiment_name: str
|
|
350
|
-
- experiment_name__in: list[str]
|
|
351
|
-
- experiment_name__contains: str
|
|
352
|
-
- experiment_name__icontains: str
|
|
353
|
-
- software_version_set_id: int
|
|
354
|
-
- software_version_set_id__in: list[int]
|
|
355
|
-
- begin_timestamp__range: tuple[datetime, datetime]
|
|
356
|
-
- end_timestamp__range: tuple[datetime, datetime]
|
|
357
|
-
- end_timestamp__isnull: bool
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
Queried runs with some query related metadata.
|
|
361
|
-
|
|
362
|
-
"""
|
|
363
216
|
params = self._clean_query_parameters(RunData, **kwargs)
|
|
364
217
|
response = self._send_request(requests.get, "runs", params=params)
|
|
365
|
-
return self.
|
|
218
|
+
return self._deserialize_response(response, RunLiteList, list_with_meta=True)
|
|
366
219
|
|
|
367
220
|
def create_observations(
|
|
368
221
|
self, observation_definitions: Sequence[ObservationDefinition]
|
|
369
222
|
) -> ListWithMeta[ObservationData]:
|
|
370
|
-
"""Create observations in the database.
|
|
371
|
-
|
|
372
|
-
Args:
|
|
373
|
-
observation_definitions: A sequence of observation definitions,
|
|
374
|
-
each containing the content of the observation which will be created.
|
|
375
|
-
|
|
376
|
-
Returns:
|
|
377
|
-
Created observations, each including also the database created fields like ID and timestamps.
|
|
378
|
-
|
|
379
|
-
"""
|
|
380
223
|
json_str = self._serialize_model(ObservationDefinitionList(observation_definitions))
|
|
381
224
|
response = self._send_request(requests.post, "observations", json_str=json_str)
|
|
382
|
-
return self.
|
|
225
|
+
return self._deserialize_response(response, ObservationDataList, list_with_meta=True)
|
|
383
226
|
|
|
384
227
|
def get_observations(
|
|
385
228
|
self,
|
|
@@ -389,40 +232,10 @@ class StationControlClient:
|
|
|
389
232
|
dut_field: str | None = None,
|
|
390
233
|
tags: list[str] | None = None,
|
|
391
234
|
invalid: bool | None = False,
|
|
392
|
-
run_ids: list[
|
|
393
|
-
sequence_ids: list[
|
|
235
|
+
run_ids: list[StrUUID] | None = None,
|
|
236
|
+
sequence_ids: list[StrUUID] | None = None,
|
|
394
237
|
limit: int | None = None,
|
|
395
238
|
) -> list[ObservationData]:
|
|
396
|
-
"""Get observations from the database.
|
|
397
|
-
|
|
398
|
-
Observations are queried by the given query parameters.
|
|
399
|
-
|
|
400
|
-
Args:
|
|
401
|
-
mode: The "mode" used to query the observations. Possible values "all_latest", "tags_and", or "tags_or".
|
|
402
|
-
|
|
403
|
-
- "all_latest":Query all the latest observations for the given ``dut_label``.
|
|
404
|
-
No other query parameters are accepted.
|
|
405
|
-
- "tags_and": Query observations. Query all the observations that have all the given ``tags``.
|
|
406
|
-
By default, only valid observations are included.
|
|
407
|
-
All other query parameters can be used to narrow down the query,
|
|
408
|
-
expect "run_ids" and "sequence_ids".
|
|
409
|
-
- "tags_or": Query all the latest observations that have at least one of the given ``tags``.
|
|
410
|
-
Additionally, ``dut_label`` must be given. No other query parameters are used.
|
|
411
|
-
- "sequence": Query observations originating from a list of run and/or sequence IDs.
|
|
412
|
-
No other query parameters are accepted.
|
|
413
|
-
dut_label: DUT label of the device the observations pertain to.
|
|
414
|
-
dut_field: Name of the property the observation is about.
|
|
415
|
-
tags: Human-readable tags of the observation.
|
|
416
|
-
invalid: Flag indicating if the object is invalid. Automated systems must not use invalid objects.
|
|
417
|
-
If ``None``, both valid and invalid objects are included.
|
|
418
|
-
run_ids: The run IDs for which to query the observations.
|
|
419
|
-
sequence_ids: The sequence IDs for which to query the observations.
|
|
420
|
-
limit: Indicates the maximum number of items to return.
|
|
421
|
-
|
|
422
|
-
Returns:
|
|
423
|
-
Observations, each including also the database created fields like ID and timestamps.
|
|
424
|
-
|
|
425
|
-
"""
|
|
426
239
|
kwargs = {
|
|
427
240
|
"mode": mode,
|
|
428
241
|
"dut_label": dut_label,
|
|
@@ -435,249 +248,125 @@ class StationControlClient:
|
|
|
435
248
|
}
|
|
436
249
|
params = self._clean_query_parameters(ObservationData, **kwargs)
|
|
437
250
|
response = self._send_request(requests.get, "observations", params=params)
|
|
438
|
-
return
|
|
251
|
+
return self._deserialize_response(response, ObservationDataList)
|
|
439
252
|
|
|
440
253
|
def query_observations(self, **kwargs) -> ListWithMeta[ObservationData]:
|
|
441
|
-
"""Query observations from the database.
|
|
442
|
-
|
|
443
|
-
Observations are queried by the given query parameters. Currently supported query parameters:
|
|
444
|
-
- observation_id: int
|
|
445
|
-
- observation_id__in: list[int]
|
|
446
|
-
- dut_label: str
|
|
447
|
-
- dut_field: str
|
|
448
|
-
- dut_field__in: list[str]
|
|
449
|
-
- tags__overlap: list[str]
|
|
450
|
-
- tags__contains: list[str]
|
|
451
|
-
- invalid: bool
|
|
452
|
-
- source__run_id__in: list[uuid.UUID]
|
|
453
|
-
- source__sequence_id__in: list[uuid.UUID]
|
|
454
|
-
- source__type: str
|
|
455
|
-
- uncertainty__isnull: bool
|
|
456
|
-
- created_timestamp__range: tuple[datetime, datetime]
|
|
457
|
-
- observation_set_ids__overlap: list[uuid.UUID]
|
|
458
|
-
- observation_set_ids__contains: list[uuid.UUID]
|
|
459
|
-
|
|
460
|
-
Returns:
|
|
461
|
-
Queried observations with some query related metadata.
|
|
462
|
-
|
|
463
|
-
"""
|
|
464
254
|
params = self._clean_query_parameters(ObservationData, **kwargs)
|
|
465
255
|
response = self._send_request(requests.get, "observations", params=params)
|
|
466
|
-
return self.
|
|
256
|
+
return self._deserialize_response(response, ObservationDataList, list_with_meta=True)
|
|
467
257
|
|
|
468
258
|
def update_observations(self, observation_updates: Sequence[ObservationUpdate]) -> list[ObservationData]:
|
|
469
|
-
"""Update observations in the database.
|
|
470
|
-
|
|
471
|
-
Args:
|
|
472
|
-
observation_updates: A sequence of observation updates,
|
|
473
|
-
each containing the content of the observation which will be updated.
|
|
474
|
-
|
|
475
|
-
Returns:
|
|
476
|
-
Updated observations, each including also the database created fields like ID and timestamps.
|
|
477
|
-
|
|
478
|
-
"""
|
|
479
259
|
json_str = self._serialize_model(ObservationUpdateList(observation_updates))
|
|
480
260
|
response = self._send_request(requests.patch, "observations", json_str=json_str)
|
|
481
|
-
return
|
|
261
|
+
return self._deserialize_response(response, ObservationDataList)
|
|
482
262
|
|
|
483
263
|
def query_observation_sets(self, **kwargs) -> ListWithMeta[ObservationSetData]:
|
|
484
|
-
"""Query observation sets from the database.
|
|
485
|
-
|
|
486
|
-
Observation sets are queried by the given query parameters. Currently supported query parameters:
|
|
487
|
-
- observation_set_id: UUID
|
|
488
|
-
- observation_set_id__in: list[UUID]
|
|
489
|
-
- observation_set_type: Literal["calibration-set", "generic-set", "quality-metric-set"]
|
|
490
|
-
- observation_ids__overlap: list[int]
|
|
491
|
-
- observation_ids__contains: list[int]
|
|
492
|
-
- describes_id: UUID
|
|
493
|
-
- describes_id__in: list[UUID]
|
|
494
|
-
- invalid: bool
|
|
495
|
-
- created_timestamp__range: tuple[datetime, datetime]
|
|
496
|
-
- end_timestamp__isnull: bool
|
|
497
|
-
- dut_label: str
|
|
498
|
-
- dut_label__in: list[str]
|
|
499
|
-
|
|
500
|
-
Returns:
|
|
501
|
-
Queried observation sets with some query related metadata
|
|
502
|
-
|
|
503
|
-
"""
|
|
504
264
|
params = self._clean_query_parameters(ObservationSetData, **kwargs)
|
|
505
265
|
response = self._send_request(requests.get, "observation-sets", params=params)
|
|
506
|
-
return self.
|
|
266
|
+
return self._deserialize_response(response, ObservationSetDataList, list_with_meta=True)
|
|
507
267
|
|
|
508
268
|
def create_observation_set(self, observation_set_definition: ObservationSetDefinition) -> ObservationSetData:
|
|
509
|
-
"""Create an observation set in the database.
|
|
510
|
-
|
|
511
|
-
Args:
|
|
512
|
-
observation_set_definition: The content of the observation set to be created.
|
|
513
|
-
|
|
514
|
-
Returns:
|
|
515
|
-
The content of the observation set.
|
|
516
|
-
|
|
517
|
-
Raises:
|
|
518
|
-
ExaError: If creation failed.
|
|
519
|
-
|
|
520
|
-
"""
|
|
521
269
|
json_str = self._serialize_model(observation_set_definition)
|
|
522
270
|
response = self._send_request(requests.post, "observation-sets", json_str=json_str)
|
|
523
|
-
return
|
|
524
|
-
|
|
525
|
-
def get_observation_set(self, observation_set_id: uuid.UUID) -> ObservationSetData:
|
|
526
|
-
"""Get an observation set from the database.
|
|
527
|
-
|
|
528
|
-
Args:
|
|
529
|
-
observation_set_id: Observation set to retrieve.
|
|
530
|
-
|
|
531
|
-
Returns:
|
|
532
|
-
The content of the observation set.
|
|
533
|
-
|
|
534
|
-
Raises:
|
|
535
|
-
ExaError: If retrieval failed.
|
|
271
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
536
272
|
|
|
537
|
-
|
|
273
|
+
def get_observation_set(self, observation_set_id: StrUUID) -> ObservationSetData:
|
|
538
274
|
response = self._send_request(requests.get, f"observation-sets/{observation_set_id}")
|
|
539
|
-
return
|
|
275
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
540
276
|
|
|
541
277
|
def update_observation_set(self, observation_set_update: ObservationSetUpdate) -> ObservationSetData:
|
|
542
|
-
"""Update an observation set in the database.
|
|
543
|
-
|
|
544
|
-
Args:
|
|
545
|
-
observation_set_update: The content of the observation set to be updated.
|
|
546
|
-
|
|
547
|
-
Returns:
|
|
548
|
-
The content of the observation set.
|
|
549
|
-
|
|
550
|
-
Raises:
|
|
551
|
-
ExaError: If updating failed.
|
|
552
|
-
|
|
553
|
-
"""
|
|
554
278
|
json_str = self._serialize_model(observation_set_update)
|
|
555
279
|
response = self._send_request(requests.patch, "observation-sets", json_str=json_str)
|
|
556
|
-
return
|
|
557
|
-
|
|
558
|
-
def finalize_observation_set(self, observation_set_id: uuid.UUID) -> None:
|
|
559
|
-
"""Finalize an observation set in the database.
|
|
560
|
-
|
|
561
|
-
A finalized set is nearly immutable, allowing to change only ``invalid`` flag after finalization.
|
|
562
|
-
|
|
563
|
-
Args:
|
|
564
|
-
observation_set_id: Observation set to finalize.
|
|
565
|
-
|
|
566
|
-
Raises:
|
|
567
|
-
ExaError: If finalization failed.
|
|
280
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
568
281
|
|
|
569
|
-
|
|
282
|
+
def finalize_observation_set(self, observation_set_id: StrUUID) -> None:
|
|
570
283
|
self._send_request(requests.post, f"observation-sets/{observation_set_id}/finalize")
|
|
571
284
|
|
|
572
|
-
def get_observation_set_observations(self, observation_set_id:
|
|
573
|
-
"""Get the constituent observations of an observation set from the database.
|
|
574
|
-
|
|
575
|
-
Args:
|
|
576
|
-
observation_set_id: UUID of the observation set to retrieve.
|
|
577
|
-
|
|
578
|
-
Returns:
|
|
579
|
-
Observations belonging to the given observation set.
|
|
580
|
-
|
|
581
|
-
"""
|
|
285
|
+
def get_observation_set_observations(self, observation_set_id: StrUUID) -> list[ObservationLite]:
|
|
582
286
|
response = self._send_request(requests.get, f"observation-sets/{observation_set_id}/observations")
|
|
583
|
-
return
|
|
287
|
+
return self._deserialize_response(response, ObservationLiteList)
|
|
584
288
|
|
|
585
|
-
def
|
|
586
|
-
|
|
289
|
+
def get_default_calibration_set(self) -> ObservationSetData:
|
|
290
|
+
response = self._send_request(requests.get, "calibration-sets/default")
|
|
291
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
587
292
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
Returns:
|
|
592
|
-
Dictionary of observations belonging to the given calibration set.
|
|
593
|
-
|
|
594
|
-
"""
|
|
595
|
-
observation_set = self.get_observation_set(calibration_set_id)
|
|
596
|
-
if observation_set.observation_set_type != "calibration-set":
|
|
597
|
-
raise ValueError("Observation set type is not 'calibration-set'")
|
|
598
|
-
observations = self.get_observation_set_observations(calibration_set_id)
|
|
599
|
-
return calset_from_observations(observations)
|
|
293
|
+
def get_default_calibration_set_observations(self) -> list[ObservationLite]:
|
|
294
|
+
response = self._send_request(requests.get, "calibration-sets/default/observations")
|
|
295
|
+
return self._deserialize_response(response, ObservationLiteList)
|
|
600
296
|
|
|
601
|
-
def
|
|
602
|
-
|
|
297
|
+
def get_default_dynamic_quantum_architecture(self) -> DynamicQuantumArchitecture:
|
|
298
|
+
response = self._send_request(requests.get, "calibration-sets/default/dynamic-quantum-architecture")
|
|
299
|
+
return self._deserialize_response(response, DynamicQuantumArchitecture)
|
|
603
300
|
|
|
604
|
-
|
|
605
|
-
|
|
301
|
+
@cache
|
|
302
|
+
def get_dynamic_quantum_architecture(self, calibration_set_id: StrUUID) -> DynamicQuantumArchitecture:
|
|
303
|
+
response = self._send_request(
|
|
304
|
+
requests.get, f"calibration-sets/{calibration_set_id}/dynamic-quantum-architecture"
|
|
305
|
+
)
|
|
306
|
+
return self._deserialize_response(response, DynamicQuantumArchitecture)
|
|
606
307
|
|
|
607
|
-
|
|
608
|
-
|
|
308
|
+
def get_default_calibration_set_quality_metrics(self) -> QualityMetrics:
|
|
309
|
+
response = self._send_request(requests.get, "calibration-sets/default/metrics")
|
|
310
|
+
return self._deserialize_response(response, QualityMetrics)
|
|
609
311
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
dut_label=dut_label,
|
|
614
|
-
invalid=False,
|
|
615
|
-
end_timestamp__isnull=False, # Finalized
|
|
616
|
-
order_by="-end_timestamp", # This requires SC version > 35.15
|
|
617
|
-
limit=1,
|
|
618
|
-
)
|
|
619
|
-
return observation_sets[0].observation_set_id
|
|
312
|
+
def get_calibration_set_quality_metrics(self, calibration_set_id: StrUUID) -> QualityMetrics:
|
|
313
|
+
response = self._send_request(requests.get, f"calibration-sets/{calibration_set_id}/metrics")
|
|
314
|
+
return self._deserialize_response(response, QualityMetrics)
|
|
620
315
|
|
|
621
316
|
def get_duts(self) -> list[DutData]:
|
|
622
|
-
"""Get DUTs of the station control."""
|
|
623
317
|
response = self._send_request(requests.get, "duts")
|
|
624
|
-
return
|
|
318
|
+
return self._deserialize_response(response, DutList)
|
|
625
319
|
|
|
626
320
|
def get_dut_fields(self, dut_label: str) -> list[DutFieldData]:
|
|
627
|
-
"""Get DUT fields for the specified DUT label from the database."""
|
|
628
321
|
params = {"dut_label": dut_label}
|
|
629
322
|
response = self._send_request(requests.get, "dut-fields", params=params)
|
|
630
|
-
return
|
|
323
|
+
return self._deserialize_response(response, DutFieldDataList)
|
|
631
324
|
|
|
632
325
|
def query_sequence_metadatas(self, **kwargs) -> ListWithMeta[SequenceMetadataData]:
|
|
633
|
-
"""Query sequence metadatas from the database.
|
|
634
|
-
|
|
635
|
-
Sequence metadatas are queried by the given query parameters. Currently supported query parameters:
|
|
636
|
-
- origin_id: str
|
|
637
|
-
- origin_id__in: list[str]
|
|
638
|
-
- origin_uri: str
|
|
639
|
-
- origin_uri__icontains: str
|
|
640
|
-
- created_timestamp__range: tuple[datetime, datetime]
|
|
641
|
-
|
|
642
|
-
Returns:
|
|
643
|
-
Sequence metadatas with some query related metadata.
|
|
644
|
-
|
|
645
|
-
"""
|
|
646
326
|
params = self._clean_query_parameters(SequenceMetadataData, **kwargs)
|
|
647
327
|
response = self._send_request(requests.get, "sequence-metadatas", params=params)
|
|
648
|
-
return self.
|
|
328
|
+
return self._deserialize_response(response, SequenceMetadataDataList, list_with_meta=True)
|
|
649
329
|
|
|
650
330
|
def create_sequence_metadata(
|
|
651
331
|
self, sequence_metadata_definition: SequenceMetadataDefinition
|
|
652
332
|
) -> SequenceMetadataData:
|
|
653
|
-
"""Create sequence metadata in the database."""
|
|
654
333
|
json_str = self._serialize_model(sequence_metadata_definition)
|
|
655
334
|
response = self._send_request(requests.post, "sequence-metadatas", json_str=json_str)
|
|
656
|
-
return
|
|
335
|
+
return self._deserialize_response(response, SequenceMetadataData)
|
|
657
336
|
|
|
658
337
|
def save_sequence_result(self, sequence_result_definition: SequenceResultDefinition) -> SequenceResultData:
|
|
659
|
-
"""Save sequence result in the database.
|
|
660
|
-
|
|
661
|
-
This method creates the object if it doesn't exist and completely replaces the "data" and "final" if it does.
|
|
662
|
-
Timestamps are assigned by the database. "modified_timestamp" is not set on initial creation,
|
|
663
|
-
but it's updated on each subsequent call.
|
|
664
|
-
"""
|
|
665
338
|
# FIXME: We don't have information if the object was created or updated. Thus, server always responds 200 (OK).
|
|
666
339
|
json_str = self._serialize_model(sequence_result_definition)
|
|
667
340
|
response = self._send_request(
|
|
668
341
|
requests.put, f"sequence-results/{sequence_result_definition.sequence_id}", json_str=json_str
|
|
669
342
|
)
|
|
670
|
-
return
|
|
343
|
+
return self._deserialize_response(response, SequenceResultData)
|
|
671
344
|
|
|
672
|
-
def get_sequence_result(self, sequence_id:
|
|
673
|
-
"""Get sequence result from the database."""
|
|
345
|
+
def get_sequence_result(self, sequence_id: StrUUID) -> SequenceResultData:
|
|
674
346
|
response = self._send_request(requests.get, f"sequence-results/{sequence_id}")
|
|
675
|
-
return
|
|
347
|
+
return self._deserialize_response(response, SequenceResultData)
|
|
348
|
+
|
|
349
|
+
@cache
|
|
350
|
+
def get_static_quantum_architecture(self, dut_label: str) -> StaticQuantumArchitecture:
|
|
351
|
+
response = self._send_request(requests.get, f"static-quantum-architectures/{dut_label}")
|
|
352
|
+
return self._deserialize_response(response, StaticQuantumArchitecture)
|
|
676
353
|
|
|
677
|
-
def get_job(self, job_id:
|
|
678
|
-
"""Get job data."""
|
|
354
|
+
def get_job(self, job_id: StrUUID) -> JobData:
|
|
679
355
|
response = self._send_request(requests.get, f"jobs/{job_id}")
|
|
680
|
-
return
|
|
356
|
+
return self._deserialize_response(response, JobData)
|
|
357
|
+
|
|
358
|
+
def abort_job(self, job_id: StrUUID) -> None:
|
|
359
|
+
self._send_request(requests.post, f"jobs/{job_id}/abort")
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def _create_signature(client_signature: str) -> str:
|
|
363
|
+
signature = f"{platform.platform(terse=True)}"
|
|
364
|
+
signature += f", python {platform.python_version()}"
|
|
365
|
+
version_string = "iqm-station-control-client"
|
|
366
|
+
signature += f", StationControlClient {version_string} {version(version_string)}"
|
|
367
|
+
if client_signature:
|
|
368
|
+
signature += f", {client_signature}"
|
|
369
|
+
return signature
|
|
681
370
|
|
|
682
371
|
def _wait_job_completion(self, job_id: str, update_progress_callback: Callable[[Statuses], None] | None) -> bool:
|
|
683
372
|
logger.info("Waiting for job ID: %s", job_id)
|
|
@@ -727,7 +416,7 @@ class StationControlClient:
|
|
|
727
416
|
|
|
728
417
|
def _poll_job(self, job_id: str) -> JobData:
|
|
729
418
|
response = self._send_request(requests.get, f"jobs/{job_id}")
|
|
730
|
-
job =
|
|
419
|
+
job = self._deserialize_response(response, JobData)
|
|
731
420
|
if job.job_status == JobExecutorStatus.FAILED:
|
|
732
421
|
raise InternalServerError(f"Job: {job.job_id}\n{job.job_error}")
|
|
733
422
|
return job
|
|
@@ -738,8 +427,6 @@ class StationControlClient:
|
|
|
738
427
|
|
|
739
428
|
All Pydantic models should be serialized using this method, to keep the client behavior uniform.
|
|
740
429
|
|
|
741
|
-
TODO add a corresponding deserialization method.
|
|
742
|
-
|
|
743
430
|
Args:
|
|
744
431
|
model: Pydantic model to JSON-serialize.
|
|
745
432
|
|
|
@@ -760,8 +447,9 @@ class StationControlClient:
|
|
|
760
447
|
octets: bytes | None = None,
|
|
761
448
|
params: dict[str, Any] | None = None,
|
|
762
449
|
headers: dict[str, str] | None = None,
|
|
450
|
+
timeout: int = 120,
|
|
763
451
|
) -> requests.Response:
|
|
764
|
-
"""Send
|
|
452
|
+
"""Send an HTTP request.
|
|
765
453
|
|
|
766
454
|
Parameters ``json_str``, ``octets`` and ``params`` are mutually exclusive.
|
|
767
455
|
The first non-None argument (in this order) will be used to construct the body of the request.
|
|
@@ -782,37 +470,15 @@ class StationControlClient:
|
|
|
782
470
|
|
|
783
471
|
"""
|
|
784
472
|
# Will raise an error if respectively an error response code is returned.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if self._enable_opentelemetry:
|
|
788
|
-
parent_span_context = trace.set_span_in_context(trace.get_current_span())
|
|
789
|
-
propagate.inject(carrier=headers, context=parent_span_context)
|
|
790
|
-
# If token callback exists, use it to retrieve the token and add it to the headers
|
|
791
|
-
if self._get_token_callback:
|
|
792
|
-
headers["Authorization"] = self._get_token_callback()
|
|
793
|
-
|
|
794
|
-
# Build request options explicitly
|
|
795
|
-
http_request_options: dict[str, Any] = {}
|
|
796
|
-
if json_str is not None:
|
|
797
|
-
# Must be able to handle JSON strings with arbitrary unicode characters, so we use an explicit
|
|
798
|
-
# encoding into bytes, and set the headers so the recipient can decode the request body correctly.
|
|
799
|
-
http_request_options["data"] = json_str.encode("utf-8")
|
|
800
|
-
headers["Content-Type"] = "application/json; charset=UTF-8"
|
|
801
|
-
elif octets is not None:
|
|
802
|
-
http_request_options["data"] = octets
|
|
803
|
-
headers["Content-Type"] = "application/octet-stream"
|
|
804
|
-
elif params is not None:
|
|
805
|
-
http_request_options["params"] = params
|
|
806
|
-
# otherwise no body-related parameter will be passed to requests
|
|
807
|
-
|
|
808
|
-
if headers:
|
|
809
|
-
# do not pass empty headers dict
|
|
810
|
-
http_request_options["headers"] = headers
|
|
473
|
+
# http_method should be any of requests.[post|get|put|head|delete|patch|options]
|
|
811
474
|
|
|
475
|
+
request_kwargs = self._build_request_kwargs(
|
|
476
|
+
json_str=json_str, octets=octets, params=params or {}, headers=headers or {}, timeout=timeout
|
|
477
|
+
)
|
|
812
478
|
url = f"{self.root_url}/{url_path}"
|
|
813
479
|
# TODO SW-1387: Use v1 API
|
|
814
480
|
# url = f"{self.root_url}/{self.version}/{url_path}"
|
|
815
|
-
response = http_method(url, **
|
|
481
|
+
response = http_method(url, **request_kwargs)
|
|
816
482
|
if not response.ok:
|
|
817
483
|
try:
|
|
818
484
|
response_json = response.json()
|
|
@@ -820,7 +486,11 @@ class StationControlClient:
|
|
|
820
486
|
except json.JSONDecodeError:
|
|
821
487
|
error_message = response.text
|
|
822
488
|
|
|
823
|
-
|
|
489
|
+
try:
|
|
490
|
+
error_class = map_from_status_code_to_error(response.status_code)
|
|
491
|
+
except KeyError:
|
|
492
|
+
raise RuntimeError(f"Unexpected response status code {response.status_code}: {error_message}")
|
|
493
|
+
|
|
824
494
|
raise error_class(error_message)
|
|
825
495
|
return response
|
|
826
496
|
|
|
@@ -859,18 +529,67 @@ class StationControlClient:
|
|
|
859
529
|
def _get_client_api_version() -> Version:
|
|
860
530
|
return parse(version("iqm-station-control-client"))
|
|
861
531
|
|
|
532
|
+
def _build_request_kwargs(self, **kwargs):
|
|
533
|
+
kwargs.setdefault("headers", {"User-Agent": self._signature})
|
|
534
|
+
|
|
535
|
+
params = kwargs.get("params", {})
|
|
536
|
+
headers = kwargs["headers"]
|
|
537
|
+
|
|
538
|
+
if kwargs["json_str"] is not None:
|
|
539
|
+
# Must be able to handle JSON strings with arbitrary unicode characters, so we use an explicit
|
|
540
|
+
# encoding into bytes, and set the headers so the recipient can decode the request body correctly.
|
|
541
|
+
data = kwargs["json_str"].encode("utf-8")
|
|
542
|
+
headers["Content-Type"] = "application/json; charset=UTF-8"
|
|
543
|
+
elif kwargs["octets"] is not None:
|
|
544
|
+
data = kwargs["octets"]
|
|
545
|
+
headers["Content-Type"] = "application/octet-stream"
|
|
546
|
+
else:
|
|
547
|
+
data = None
|
|
548
|
+
if self._enable_opentelemetry:
|
|
549
|
+
parent_span_context = trace.set_span_in_context(trace.get_current_span())
|
|
550
|
+
propagate.inject(carrier=headers, context=parent_span_context)
|
|
551
|
+
|
|
552
|
+
# If token callback exists, use it to retrieve the token and add it to the headers
|
|
553
|
+
if self._get_token_callback:
|
|
554
|
+
headers["Authorization"] = self._get_token_callback()
|
|
555
|
+
|
|
556
|
+
kwargs = {
|
|
557
|
+
"params": params,
|
|
558
|
+
"data": data,
|
|
559
|
+
"headers": headers,
|
|
560
|
+
"timeout": kwargs["timeout"],
|
|
561
|
+
}
|
|
562
|
+
return _remove_empty_values(kwargs)
|
|
563
|
+
|
|
862
564
|
@staticmethod
|
|
863
565
|
def _clean_query_parameters(model: Any, **kwargs) -> dict[str, Any]:
|
|
864
566
|
if issubclass(model, PydanticBase) and "invalid" in model.model_fields.keys() and "invalid" not in kwargs:
|
|
865
567
|
# Get only valid items by default, "invalid=None" would return also invalid ones.
|
|
866
568
|
# This default has to be set on the client side, server side uses default "None".
|
|
867
569
|
kwargs["invalid"] = False
|
|
868
|
-
|
|
869
|
-
return {key: value for key, value in kwargs.items() if value not in [None, {}]}
|
|
570
|
+
return _remove_empty_values(kwargs)
|
|
870
571
|
|
|
871
572
|
@staticmethod
|
|
872
|
-
def
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
573
|
+
def _deserialize_response(
|
|
574
|
+
response: requests.Response,
|
|
575
|
+
model_class: type[TypePydanticBase] | type[ListModel[list[TypePydanticBase]]],
|
|
576
|
+
*,
|
|
577
|
+
list_with_meta: bool = False,
|
|
578
|
+
) -> TypePydanticBase | ListWithMeta[TypePydanticBase]:
|
|
579
|
+
# Use "model_validate_json(response.text)" instead of "model_validate(response.json())".
|
|
580
|
+
# This validates the provided data as a JSON string or bytes object.
|
|
581
|
+
# If your incoming data is a JSON payload, this is generally considered faster.
|
|
582
|
+
if list_with_meta:
|
|
583
|
+
response_with_meta = ResponseWithMeta.model_validate_json(response.text)
|
|
584
|
+
if response_with_meta.meta and response_with_meta.meta.errors:
|
|
585
|
+
logger.warning(
|
|
586
|
+
"Errors in station control response:\n - %s", "\n - ".join(response_with_meta.meta.errors)
|
|
587
|
+
)
|
|
588
|
+
return ListWithMeta(model_class.model_validate(response_with_meta.items), meta=response_with_meta.meta)
|
|
589
|
+
model = model_class.model_validate_json(response.text)
|
|
590
|
+
return model
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _remove_empty_values(kwargs):
|
|
594
|
+
# Remove None and {} values
|
|
595
|
+
return {key: value for key, value in kwargs.items() if value not in [None, {}]}
|