iqm-station-control-client 8.1.0__py3-none-any.whl → 9.1.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 +179 -46
- 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 +245 -487
- 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/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.1.0.dist-info → iqm_station_control_client-9.1.0.dist-info}/METADATA +2 -2
- {iqm_station_control_client-8.1.0.dist-info → iqm_station_control_client-9.1.0.dist-info}/RECORD +35 -33
- iqm/station_control/client/iqm_server/meta_class.py +0 -38
- {iqm_station_control_client-8.1.0.dist-info → iqm_station_control_client-9.1.0.dist-info}/LICENSE.txt +0 -0
- {iqm_station_control_client-8.1.0.dist-info → iqm_station_control_client-9.1.0.dist-info}/WHEEL +0 -0
- {iqm_station_control_client-8.1.0.dist-info → iqm_station_control_client-9.1.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,210 @@ 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
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
Current implementation uses HTTP calls to the remote station control service,
|
|
102
|
-
that is controlling the station control instance.
|
|
103
|
+
class _StationControlClientBase(StationControlInterface):
|
|
104
|
+
"""Shared functionality for StationControlClient and IqmServerClient.
|
|
103
105
|
|
|
104
106
|
Args:
|
|
105
|
-
root_url: Remote
|
|
106
|
-
get_token_callback: A callback function that returns a token
|
|
107
|
-
in all requests.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
107
|
+
root_url: Remote server URL.
|
|
108
|
+
get_token_callback: A callback function that returns a token
|
|
109
|
+
which will be passed in Authorization header in all requests.
|
|
110
|
+
client_signature: String that is added to the User-Agent header of requests
|
|
111
|
+
sent to the server.
|
|
112
|
+
enable_opentelemetry: Iff True, enable Jaeger/OpenTelemetry tracing.
|
|
150
113
|
|
|
151
114
|
"""
|
|
152
115
|
|
|
153
|
-
def __init__(
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
root_url: str,
|
|
119
|
+
*,
|
|
120
|
+
get_token_callback: Callable[[], str] | None = None,
|
|
121
|
+
client_signature: str | None = None,
|
|
122
|
+
enable_opentelemetry: bool = False,
|
|
123
|
+
):
|
|
154
124
|
self.root_url = root_url
|
|
155
|
-
self._enable_opentelemetry = os.environ.get("JAEGER_OPENTELEMETRY_COLLECTOR_ENDPOINT", None) is not None
|
|
156
125
|
self._get_token_callback = get_token_callback
|
|
157
|
-
|
|
158
|
-
self.
|
|
159
|
-
|
|
160
|
-
|
|
126
|
+
self._signature = self._create_signature(client_signature)
|
|
127
|
+
self._enable_opentelemetry = enable_opentelemetry
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def _create_signature(cls, client_signature: str | None) -> str:
|
|
131
|
+
signature = f"{platform.platform(terse=True)}"
|
|
132
|
+
signature += f", python {platform.python_version()}"
|
|
133
|
+
dist_pkg_name = "iqm-station-control-client"
|
|
134
|
+
signature += f", {cls.__name__} {dist_pkg_name} {version(dist_pkg_name)}"
|
|
135
|
+
if client_signature:
|
|
136
|
+
signature += f", {client_signature}"
|
|
137
|
+
return signature
|
|
161
138
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
139
|
+
def _send_request(
|
|
140
|
+
self,
|
|
141
|
+
http_method: Callable[..., requests.Response],
|
|
142
|
+
url_path: str,
|
|
143
|
+
*,
|
|
144
|
+
headers: dict[str, str] | None = None,
|
|
145
|
+
params: dict[str, Any] | None = None,
|
|
146
|
+
json_str: str | None = None,
|
|
147
|
+
octets: bytes | None = None,
|
|
148
|
+
timeout: int = 120,
|
|
149
|
+
) -> requests.Response:
|
|
150
|
+
"""Send an HTTP request.
|
|
170
151
|
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
152
|
+
Parameters ``json_str`` and ``octets`` are mutually exclusive.
|
|
153
|
+
The first non-None argument (in this order) will be used to construct the body of the request.
|
|
175
154
|
|
|
176
155
|
Args:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
156
|
+
http_method: HTTP method to use for the request, any of requests.[post|get|put|head|delete|patch|options].
|
|
157
|
+
url_path: URL for the request.
|
|
158
|
+
headers: Additional HTTP headers for the request. Some may be overridden.
|
|
159
|
+
params: HTTP query parameters to store in the query string of the request URL.
|
|
160
|
+
json_str: JSON string to store in the body, may contain arbitrary Unicode characters.
|
|
161
|
+
octets: Pre-serialized binary data to store in the body.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Response to the request.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
StationControlError: Request was not successful.
|
|
181
168
|
|
|
182
169
|
"""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
170
|
+
# Will raise an error if respectively an error response code is returned.
|
|
171
|
+
# http_method should be any of requests.[post|get|put|head|delete|patch|options]
|
|
172
|
+
|
|
173
|
+
request_kwargs = self._build_request_kwargs(
|
|
174
|
+
headers=headers or {}, params=params or {}, json_str=json_str, octets=octets, timeout=timeout
|
|
175
|
+
)
|
|
176
|
+
url = f"{self.root_url}/{url_path}"
|
|
177
|
+
# TODO SW-1387: Use v1 API
|
|
178
|
+
# url = f"{self.root_url}/{self.version}/{url_path}"
|
|
179
|
+
response = http_method(url, **request_kwargs)
|
|
180
|
+
if not response.ok:
|
|
181
|
+
try:
|
|
182
|
+
response_json = response.json()
|
|
183
|
+
error_message = response_json["detail"]
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
error_message = response.text
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
error_class = map_from_status_code_to_error(response.status_code)
|
|
189
|
+
except KeyError:
|
|
190
|
+
raise RuntimeError(f"Unexpected response status code {response.status_code}: {error_message}")
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
raise error_class(error_message)
|
|
193
|
+
return response
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
def _build_request_kwargs(
|
|
196
|
+
self,
|
|
197
|
+
*,
|
|
198
|
+
headers: dict[str, str],
|
|
199
|
+
params: dict[str, Any],
|
|
200
|
+
json_str: str | None = None,
|
|
201
|
+
octets: bytes | None = None,
|
|
202
|
+
timeout: int,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Prepare the keyword arguments for an HTTP request."""
|
|
205
|
+
# add default headers
|
|
206
|
+
headers["User-Agent"] = self._signature
|
|
207
|
+
|
|
208
|
+
# json_str and octets are mutually exclusive
|
|
209
|
+
data: bytes | None = None
|
|
210
|
+
if json_str is not None:
|
|
211
|
+
# Must be able to handle JSON strings with arbitrary unicode characters, so we use an explicit
|
|
212
|
+
# encoding into bytes, and set the headers so the recipient can decode the request body correctly.
|
|
213
|
+
data = json_str.encode("utf-8")
|
|
214
|
+
headers["Content-Type"] = "application/json; charset=UTF-8"
|
|
215
|
+
elif octets is not None:
|
|
216
|
+
data = octets
|
|
217
|
+
headers["Content-Type"] = "application/octet-stream"
|
|
199
218
|
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
if self._enable_opentelemetry:
|
|
220
|
+
parent_span_context = trace.set_span_in_context(trace.get_current_span())
|
|
221
|
+
propagate.inject(carrier=headers, context=parent_span_context)
|
|
222
|
+
|
|
223
|
+
# If token callback exists, use it to retrieve the token and add it to the headers
|
|
224
|
+
if self._get_token_callback:
|
|
225
|
+
headers["Authorization"] = self._get_token_callback()
|
|
226
|
+
|
|
227
|
+
kwargs = {
|
|
228
|
+
"headers": headers,
|
|
229
|
+
"params": params,
|
|
230
|
+
"data": data,
|
|
231
|
+
"timeout": timeout,
|
|
232
|
+
}
|
|
233
|
+
return _remove_empty_values(kwargs)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class StationControlClient(_StationControlClientBase):
|
|
237
|
+
"""Client implementation for station control service REST API.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
root_url: Remote station control service URL.
|
|
241
|
+
get_token_callback: A callback function that returns a token (str)
|
|
242
|
+
which will be passed in Authorization header in all requests.
|
|
243
|
+
client_signature: String that is added to the User-Agent header of requests
|
|
244
|
+
sent to the server.
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self, root_url: str, *, get_token_callback: Callable[[], str] | None = None, client_signature: str | None = None
|
|
250
|
+
):
|
|
251
|
+
super().__init__(
|
|
252
|
+
root_url,
|
|
253
|
+
get_token_callback=get_token_callback,
|
|
254
|
+
client_signature=client_signature,
|
|
255
|
+
enable_opentelemetry=os.environ.get("JAEGER_OPENTELEMETRY_COLLECTOR_ENDPOINT", None) is not None,
|
|
256
|
+
)
|
|
257
|
+
# TODO SW-1387: Remove this when using v1 API, not needed
|
|
258
|
+
self._check_api_versions()
|
|
259
|
+
qcm_data_url = os.environ.get("CHIP_DESIGN_RECORD_FALLBACK_URL", None)
|
|
260
|
+
self._qcm_data_client = QCMDataClient(qcm_data_url) if qcm_data_url else None
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def version(self) -> str:
|
|
264
|
+
"""Return the version of the station control API this client is using."""
|
|
265
|
+
return "v1"
|
|
202
266
|
|
|
203
267
|
@cache
|
|
204
268
|
def get_about(self) -> dict:
|
|
205
|
-
"""Return information about the station control."""
|
|
206
269
|
response = self._send_request(requests.get, "about")
|
|
207
270
|
return response.json()
|
|
208
271
|
|
|
272
|
+
def get_health(self) -> dict:
|
|
273
|
+
response = self._send_request(requests.get, "health")
|
|
274
|
+
return response.json()
|
|
275
|
+
|
|
209
276
|
@cache
|
|
210
277
|
def get_configuration(self) -> dict:
|
|
211
|
-
"""Return the configuration of the station control."""
|
|
212
278
|
response = self._send_request(requests.get, "configuration")
|
|
213
279
|
return response.json()
|
|
214
280
|
|
|
215
281
|
@cache
|
|
216
282
|
def get_exa_configuration(self) -> str:
|
|
217
|
-
"""Return the recommended EXA configuration from the server."""
|
|
218
283
|
response = self._send_request(requests.get, "exa/configuration")
|
|
219
284
|
return response.content.decode("utf-8")
|
|
220
285
|
|
|
221
286
|
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
287
|
# FIXME: We don't have information if the object was created or fetched. Thus, server always responds 200 (OK).
|
|
224
288
|
json_str = json.dumps(software_version_set)
|
|
225
289
|
response = self._send_request(requests.post, "software-version-sets", json_str=json_str)
|
|
226
290
|
return int(response.content)
|
|
227
291
|
|
|
228
292
|
def get_settings(self) -> SettingNode:
|
|
229
|
-
"""Return a tree representation of the default settings as defined in the configuration file."""
|
|
230
293
|
return self._get_cached_settings().model_copy()
|
|
231
294
|
|
|
232
295
|
@cache
|
|
@@ -236,7 +299,6 @@ class StationControlClient:
|
|
|
236
299
|
|
|
237
300
|
@cache
|
|
238
301
|
def get_chip_design_record(self, dut_label: str) -> dict:
|
|
239
|
-
"""Get a raw chip design record matching the given chip label."""
|
|
240
302
|
try:
|
|
241
303
|
response = self._send_request(requests.get, f"chip-design-records/{dut_label}")
|
|
242
304
|
except StationControlError as err:
|
|
@@ -247,17 +309,8 @@ class StationControlClient:
|
|
|
247
309
|
|
|
248
310
|
@cache
|
|
249
311
|
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
312
|
headers = {"accept": "application/octet-stream"}
|
|
260
|
-
response = self._send_request(requests.get, "channel-properties
|
|
313
|
+
response = self._send_request(requests.get, "channel-properties", headers=headers)
|
|
261
314
|
decoded_dict = unpack_channel_properties(response.content)
|
|
262
315
|
return decoded_dict
|
|
263
316
|
|
|
@@ -265,49 +318,17 @@ class StationControlClient:
|
|
|
265
318
|
self,
|
|
266
319
|
sweep_definition: SweepDefinition,
|
|
267
320
|
) -> 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
321
|
data = serialize_sweep_job_request(sweep_definition, queue_name="sweeps")
|
|
290
322
|
return self._send_request(requests.post, "sweeps", octets=data).json()
|
|
291
323
|
|
|
292
|
-
def get_sweep(self, sweep_id:
|
|
293
|
-
"""Get N-dimensional sweep data from the database."""
|
|
324
|
+
def get_sweep(self, sweep_id: StrUUID) -> SweepData:
|
|
294
325
|
response = self._send_request(requests.get, f"sweeps/{sweep_id}")
|
|
295
326
|
return deserialize_sweep_data(response.json())
|
|
296
327
|
|
|
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."""
|
|
328
|
+
def delete_sweep(self, sweep_id: StrUUID) -> None:
|
|
307
329
|
self._send_request(requests.delete, f"sweeps/{sweep_id}")
|
|
308
330
|
|
|
309
|
-
def get_sweep_results(self, sweep_id:
|
|
310
|
-
"""Get N-dimensional sweep results from the database."""
|
|
331
|
+
def get_sweep_results(self, sweep_id: StrUUID) -> SweepResults:
|
|
311
332
|
response = self._send_request(requests.get, f"sweeps/{sweep_id}/results")
|
|
312
333
|
return deserialize_sweep_results(response.content)
|
|
313
334
|
|
|
@@ -317,7 +338,6 @@ class StationControlClient:
|
|
|
317
338
|
update_progress_callback: Callable[[Statuses], None] | None = None,
|
|
318
339
|
wait_job_completion: bool = True,
|
|
319
340
|
) -> bool:
|
|
320
|
-
"""Execute an N-dimensional sweep of selected variables and save run, sweep and results."""
|
|
321
341
|
data = serialize_run_job_request(run_definition, queue_name="sweeps")
|
|
322
342
|
|
|
323
343
|
response = self._send_request(requests.post, "runs", octets=data)
|
|
@@ -325,61 +345,21 @@ class StationControlClient:
|
|
|
325
345
|
return self._wait_job_completion(response.json()["job_id"], update_progress_callback)
|
|
326
346
|
return False
|
|
327
347
|
|
|
328
|
-
def get_run(self, run_id:
|
|
329
|
-
"""Get run data from the database."""
|
|
348
|
+
def get_run(self, run_id: StrUUID) -> RunData:
|
|
330
349
|
response = self._send_request(requests.get, f"runs/{run_id}")
|
|
331
350
|
return deserialize_run_data(response.json())
|
|
332
351
|
|
|
333
352
|
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
353
|
params = self._clean_query_parameters(RunData, **kwargs)
|
|
364
354
|
response = self._send_request(requests.get, "runs", params=params)
|
|
365
|
-
return self.
|
|
355
|
+
return self._deserialize_response(response, RunLiteList, list_with_meta=True)
|
|
366
356
|
|
|
367
357
|
def create_observations(
|
|
368
358
|
self, observation_definitions: Sequence[ObservationDefinition]
|
|
369
359
|
) -> 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
360
|
json_str = self._serialize_model(ObservationDefinitionList(observation_definitions))
|
|
381
361
|
response = self._send_request(requests.post, "observations", json_str=json_str)
|
|
382
|
-
return self.
|
|
362
|
+
return self._deserialize_response(response, ObservationDataList, list_with_meta=True)
|
|
383
363
|
|
|
384
364
|
def get_observations(
|
|
385
365
|
self,
|
|
@@ -389,40 +369,10 @@ class StationControlClient:
|
|
|
389
369
|
dut_field: str | None = None,
|
|
390
370
|
tags: list[str] | None = None,
|
|
391
371
|
invalid: bool | None = False,
|
|
392
|
-
run_ids: list[
|
|
393
|
-
sequence_ids: list[
|
|
372
|
+
run_ids: list[StrUUID] | None = None,
|
|
373
|
+
sequence_ids: list[StrUUID] | None = None,
|
|
394
374
|
limit: int | None = None,
|
|
395
375
|
) -> 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
376
|
kwargs = {
|
|
427
377
|
"mode": mode,
|
|
428
378
|
"dut_label": dut_label,
|
|
@@ -435,249 +385,115 @@ class StationControlClient:
|
|
|
435
385
|
}
|
|
436
386
|
params = self._clean_query_parameters(ObservationData, **kwargs)
|
|
437
387
|
response = self._send_request(requests.get, "observations", params=params)
|
|
438
|
-
return
|
|
388
|
+
return self._deserialize_response(response, ObservationDataList)
|
|
439
389
|
|
|
440
390
|
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
391
|
params = self._clean_query_parameters(ObservationData, **kwargs)
|
|
465
392
|
response = self._send_request(requests.get, "observations", params=params)
|
|
466
|
-
return self.
|
|
393
|
+
return self._deserialize_response(response, ObservationDataList, list_with_meta=True)
|
|
467
394
|
|
|
468
395
|
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
396
|
json_str = self._serialize_model(ObservationUpdateList(observation_updates))
|
|
480
397
|
response = self._send_request(requests.patch, "observations", json_str=json_str)
|
|
481
|
-
return
|
|
398
|
+
return self._deserialize_response(response, ObservationDataList)
|
|
482
399
|
|
|
483
400
|
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
401
|
params = self._clean_query_parameters(ObservationSetData, **kwargs)
|
|
505
402
|
response = self._send_request(requests.get, "observation-sets", params=params)
|
|
506
|
-
return self.
|
|
403
|
+
return self._deserialize_response(response, ObservationSetDataList, list_with_meta=True)
|
|
507
404
|
|
|
508
405
|
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
406
|
json_str = self._serialize_model(observation_set_definition)
|
|
522
407
|
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.
|
|
408
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
536
409
|
|
|
537
|
-
|
|
410
|
+
def get_observation_set(self, observation_set_id: StrUUID) -> ObservationSetData:
|
|
538
411
|
response = self._send_request(requests.get, f"observation-sets/{observation_set_id}")
|
|
539
|
-
return
|
|
412
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
540
413
|
|
|
541
414
|
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
415
|
json_str = self._serialize_model(observation_set_update)
|
|
555
416
|
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.
|
|
417
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
568
418
|
|
|
569
|
-
|
|
419
|
+
def finalize_observation_set(self, observation_set_id: StrUUID) -> None:
|
|
570
420
|
self._send_request(requests.post, f"observation-sets/{observation_set_id}/finalize")
|
|
571
421
|
|
|
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
|
-
"""
|
|
422
|
+
def get_observation_set_observations(self, observation_set_id: StrUUID) -> list[ObservationLite]:
|
|
582
423
|
response = self._send_request(requests.get, f"observation-sets/{observation_set_id}/observations")
|
|
583
|
-
return
|
|
584
|
-
|
|
585
|
-
def get_calibration_set_values(self, calibration_set_id: uuid.UUID) -> dict[str, ObservationValue]:
|
|
586
|
-
"""Get saved calibration set observations by UUID
|
|
587
|
-
|
|
588
|
-
Args:
|
|
589
|
-
calibration_set_id: UUID of the calibration set to retrieve.
|
|
424
|
+
return self._deserialize_response(response, ObservationLiteList)
|
|
590
425
|
|
|
591
|
-
|
|
592
|
-
|
|
426
|
+
def get_default_calibration_set(self) -> ObservationSetData:
|
|
427
|
+
response = self._send_request(requests.get, "calibration-sets/default")
|
|
428
|
+
return self._deserialize_response(response, ObservationSetData)
|
|
593
429
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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)
|
|
430
|
+
def get_default_calibration_set_observations(self) -> list[ObservationLite]:
|
|
431
|
+
response = self._send_request(requests.get, "calibration-sets/default/observations")
|
|
432
|
+
return self._deserialize_response(response, ObservationLiteList)
|
|
600
433
|
|
|
601
|
-
def
|
|
602
|
-
|
|
434
|
+
def get_default_dynamic_quantum_architecture(self) -> DynamicQuantumArchitecture:
|
|
435
|
+
response = self._send_request(requests.get, "calibration-sets/default/dynamic-quantum-architecture")
|
|
436
|
+
return self._deserialize_response(response, DynamicQuantumArchitecture)
|
|
603
437
|
|
|
604
|
-
|
|
605
|
-
|
|
438
|
+
@cache
|
|
439
|
+
def get_dynamic_quantum_architecture(self, calibration_set_id: StrUUID) -> DynamicQuantumArchitecture:
|
|
440
|
+
response = self._send_request(
|
|
441
|
+
requests.get, f"calibration-sets/{calibration_set_id}/dynamic-quantum-architecture"
|
|
442
|
+
)
|
|
443
|
+
return self._deserialize_response(response, DynamicQuantumArchitecture)
|
|
606
444
|
|
|
607
|
-
|
|
608
|
-
|
|
445
|
+
def get_default_calibration_set_quality_metrics(self) -> QualityMetrics:
|
|
446
|
+
response = self._send_request(requests.get, "calibration-sets/default/metrics")
|
|
447
|
+
return self._deserialize_response(response, QualityMetrics)
|
|
609
448
|
|
|
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
|
|
449
|
+
def get_calibration_set_quality_metrics(self, calibration_set_id: StrUUID) -> QualityMetrics:
|
|
450
|
+
response = self._send_request(requests.get, f"calibration-sets/{calibration_set_id}/metrics")
|
|
451
|
+
return self._deserialize_response(response, QualityMetrics)
|
|
620
452
|
|
|
621
453
|
def get_duts(self) -> list[DutData]:
|
|
622
|
-
"""Get DUTs of the station control."""
|
|
623
454
|
response = self._send_request(requests.get, "duts")
|
|
624
|
-
return
|
|
455
|
+
return self._deserialize_response(response, DutList)
|
|
625
456
|
|
|
626
457
|
def get_dut_fields(self, dut_label: str) -> list[DutFieldData]:
|
|
627
|
-
"""Get DUT fields for the specified DUT label from the database."""
|
|
628
458
|
params = {"dut_label": dut_label}
|
|
629
459
|
response = self._send_request(requests.get, "dut-fields", params=params)
|
|
630
|
-
return
|
|
460
|
+
return self._deserialize_response(response, DutFieldDataList)
|
|
631
461
|
|
|
632
462
|
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
463
|
params = self._clean_query_parameters(SequenceMetadataData, **kwargs)
|
|
647
464
|
response = self._send_request(requests.get, "sequence-metadatas", params=params)
|
|
648
|
-
return self.
|
|
465
|
+
return self._deserialize_response(response, SequenceMetadataDataList, list_with_meta=True)
|
|
649
466
|
|
|
650
467
|
def create_sequence_metadata(
|
|
651
468
|
self, sequence_metadata_definition: SequenceMetadataDefinition
|
|
652
469
|
) -> SequenceMetadataData:
|
|
653
|
-
"""Create sequence metadata in the database."""
|
|
654
470
|
json_str = self._serialize_model(sequence_metadata_definition)
|
|
655
471
|
response = self._send_request(requests.post, "sequence-metadatas", json_str=json_str)
|
|
656
|
-
return
|
|
472
|
+
return self._deserialize_response(response, SequenceMetadataData)
|
|
657
473
|
|
|
658
474
|
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
475
|
# FIXME: We don't have information if the object was created or updated. Thus, server always responds 200 (OK).
|
|
666
476
|
json_str = self._serialize_model(sequence_result_definition)
|
|
667
477
|
response = self._send_request(
|
|
668
478
|
requests.put, f"sequence-results/{sequence_result_definition.sequence_id}", json_str=json_str
|
|
669
479
|
)
|
|
670
|
-
return
|
|
480
|
+
return self._deserialize_response(response, SequenceResultData)
|
|
671
481
|
|
|
672
|
-
def get_sequence_result(self, sequence_id:
|
|
673
|
-
"""Get sequence result from the database."""
|
|
482
|
+
def get_sequence_result(self, sequence_id: StrUUID) -> SequenceResultData:
|
|
674
483
|
response = self._send_request(requests.get, f"sequence-results/{sequence_id}")
|
|
675
|
-
return
|
|
484
|
+
return self._deserialize_response(response, SequenceResultData)
|
|
485
|
+
|
|
486
|
+
@cache
|
|
487
|
+
def get_static_quantum_architecture(self, dut_label: str) -> StaticQuantumArchitecture:
|
|
488
|
+
response = self._send_request(requests.get, f"static-quantum-architectures/{dut_label}")
|
|
489
|
+
return self._deserialize_response(response, StaticQuantumArchitecture)
|
|
676
490
|
|
|
677
|
-
def get_job(self, job_id:
|
|
678
|
-
"""Get job data."""
|
|
491
|
+
def get_job(self, job_id: StrUUID) -> JobData:
|
|
679
492
|
response = self._send_request(requests.get, f"jobs/{job_id}")
|
|
680
|
-
return
|
|
493
|
+
return self._deserialize_response(response, JobData)
|
|
494
|
+
|
|
495
|
+
def abort_job(self, job_id: StrUUID) -> None:
|
|
496
|
+
self._send_request(requests.post, f"jobs/{job_id}/abort")
|
|
681
497
|
|
|
682
498
|
def _wait_job_completion(self, job_id: str, update_progress_callback: Callable[[Statuses], None] | None) -> bool:
|
|
683
499
|
logger.info("Waiting for job ID: %s", job_id)
|
|
@@ -727,7 +543,7 @@ class StationControlClient:
|
|
|
727
543
|
|
|
728
544
|
def _poll_job(self, job_id: str) -> JobData:
|
|
729
545
|
response = self._send_request(requests.get, f"jobs/{job_id}")
|
|
730
|
-
job =
|
|
546
|
+
job = self._deserialize_response(response, JobData)
|
|
731
547
|
if job.job_status == JobExecutorStatus.FAILED:
|
|
732
548
|
raise InternalServerError(f"Job: {job.job_id}\n{job.job_error}")
|
|
733
549
|
return job
|
|
@@ -738,8 +554,6 @@ class StationControlClient:
|
|
|
738
554
|
|
|
739
555
|
All Pydantic models should be serialized using this method, to keep the client behavior uniform.
|
|
740
556
|
|
|
741
|
-
TODO add a corresponding deserialization method.
|
|
742
|
-
|
|
743
557
|
Args:
|
|
744
558
|
model: Pydantic model to JSON-serialize.
|
|
745
559
|
|
|
@@ -751,79 +565,6 @@ class StationControlClient:
|
|
|
751
565
|
# using the \uXXXX syntax, BaseModel.model_dump_json() keeps them in the produced JSON str.
|
|
752
566
|
return model.model_dump_json()
|
|
753
567
|
|
|
754
|
-
def _send_request(
|
|
755
|
-
self,
|
|
756
|
-
http_method: Callable[..., requests.Response],
|
|
757
|
-
url_path: str,
|
|
758
|
-
*,
|
|
759
|
-
json_str: str | None = None,
|
|
760
|
-
octets: bytes | None = None,
|
|
761
|
-
params: dict[str, Any] | None = None,
|
|
762
|
-
headers: dict[str, str] | None = None,
|
|
763
|
-
) -> requests.Response:
|
|
764
|
-
"""Send a HTTP request.
|
|
765
|
-
|
|
766
|
-
Parameters ``json_str``, ``octets`` and ``params`` are mutually exclusive.
|
|
767
|
-
The first non-None argument (in this order) will be used to construct the body of the request.
|
|
768
|
-
|
|
769
|
-
Args:
|
|
770
|
-
http_method: HTTP method to use for the request, any of requests.[post|get|put|head|delete|patch|options].
|
|
771
|
-
url_path: URL for the request.
|
|
772
|
-
json_str: JSON string to store in the body, may contain arbitrary Unicode characters.
|
|
773
|
-
octets: Pre-serialized binary data to store in the body.
|
|
774
|
-
params: HTTP query to store in the body.
|
|
775
|
-
headers: Additional HTTP headers for the request. Some may be overridden.
|
|
776
|
-
|
|
777
|
-
Returns:
|
|
778
|
-
Response to the request.
|
|
779
|
-
|
|
780
|
-
Raises:
|
|
781
|
-
StationControlError: Request was not successful.
|
|
782
|
-
|
|
783
|
-
"""
|
|
784
|
-
# Will raise an error if respectively an error response code is returned.
|
|
785
|
-
headers = headers or {}
|
|
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
|
|
811
|
-
|
|
812
|
-
url = f"{self.root_url}/{url_path}"
|
|
813
|
-
# TODO SW-1387: Use v1 API
|
|
814
|
-
# url = f"{self.root_url}/{self.version}/{url_path}"
|
|
815
|
-
response = http_method(url, **http_request_options)
|
|
816
|
-
if not response.ok:
|
|
817
|
-
try:
|
|
818
|
-
response_json = response.json()
|
|
819
|
-
error_message = response_json["detail"]
|
|
820
|
-
except json.JSONDecodeError:
|
|
821
|
-
error_message = response.text
|
|
822
|
-
|
|
823
|
-
error_class = map_from_status_code_to_error(response.status_code)
|
|
824
|
-
raise error_class(error_message)
|
|
825
|
-
return response
|
|
826
|
-
|
|
827
568
|
# TODO SW-1387: Remove this when using v1 API, not needed
|
|
828
569
|
def _check_api_versions(self):
|
|
829
570
|
client_api_version = self._get_client_api_version()
|
|
@@ -865,12 +606,29 @@ class StationControlClient:
|
|
|
865
606
|
# Get only valid items by default, "invalid=None" would return also invalid ones.
|
|
866
607
|
# This default has to be set on the client side, server side uses default "None".
|
|
867
608
|
kwargs["invalid"] = False
|
|
868
|
-
|
|
869
|
-
return {key: value for key, value in kwargs.items() if value not in [None, {}]}
|
|
609
|
+
return _remove_empty_values(kwargs)
|
|
870
610
|
|
|
871
611
|
@staticmethod
|
|
872
|
-
def
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
612
|
+
def _deserialize_response(
|
|
613
|
+
response: requests.Response,
|
|
614
|
+
model_class: type[TypePydanticBase] | type[ListModel[list[TypePydanticBase]]],
|
|
615
|
+
*,
|
|
616
|
+
list_with_meta: bool = False,
|
|
617
|
+
) -> TypePydanticBase | ListWithMeta[TypePydanticBase]:
|
|
618
|
+
# Use "model_validate_json(response.text)" instead of "model_validate(response.json())".
|
|
619
|
+
# This validates the provided data as a JSON string or bytes object.
|
|
620
|
+
# If your incoming data is a JSON payload, this is generally considered faster.
|
|
621
|
+
if list_with_meta:
|
|
622
|
+
response_with_meta = ResponseWithMeta.model_validate_json(response.text)
|
|
623
|
+
if response_with_meta.meta and response_with_meta.meta.errors:
|
|
624
|
+
logger.warning(
|
|
625
|
+
"Errors in station control response:\n - %s", "\n - ".join(response_with_meta.meta.errors)
|
|
626
|
+
)
|
|
627
|
+
return ListWithMeta(model_class.model_validate(response_with_meta.items), meta=response_with_meta.meta)
|
|
628
|
+
model = model_class.model_validate_json(response.text)
|
|
629
|
+
return model
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _remove_empty_values(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
633
|
+
"""Return a copy of the given dict without values that are None or {}."""
|
|
634
|
+
return {key: value for key, value in kwargs.items() if value not in [None, {}]}
|