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.
Files changed (38) hide show
  1. iqm/station_control/client/__init__.py +1 -1
  2. iqm/station_control/client/iqm_server/error.py +1 -1
  3. iqm/station_control/client/iqm_server/grpc_utils.py +5 -3
  4. iqm/station_control/client/iqm_server/iqm_server_client.py +276 -45
  5. iqm/station_control/client/list_models.py +18 -12
  6. iqm/station_control/client/serializers/__init__.py +1 -1
  7. iqm/station_control/client/serializers/channel_property_serializer.py +12 -6
  8. iqm/station_control/client/serializers/datetime_serializers.py +1 -1
  9. iqm/station_control/client/serializers/playlist_serializers.py +1 -1
  10. iqm/station_control/client/serializers/run_serializers.py +1 -1
  11. iqm/station_control/client/serializers/setting_node_serializer.py +1 -1
  12. iqm/station_control/client/serializers/struct_serializer.py +1 -1
  13. iqm/station_control/client/serializers/sweep_serializers.py +2 -3
  14. iqm/station_control/client/serializers/task_serializers.py +1 -1
  15. iqm/station_control/client/station_control.py +162 -443
  16. iqm/station_control/client/utils.py +44 -17
  17. iqm/station_control/interface/__init__.py +1 -1
  18. iqm/station_control/interface/list_with_meta.py +1 -1
  19. iqm/station_control/interface/models/__init__.py +13 -1
  20. iqm/station_control/interface/models/dut.py +1 -1
  21. iqm/station_control/interface/models/dynamic_quantum_architecture.py +98 -0
  22. iqm/station_control/interface/models/jobs.py +27 -3
  23. iqm/station_control/interface/models/observation.py +1 -1
  24. iqm/station_control/interface/models/observation_set.py +15 -1
  25. iqm/station_control/interface/models/run.py +1 -1
  26. iqm/station_control/interface/models/sequence.py +1 -1
  27. iqm/station_control/interface/models/static_quantum_architecture.py +40 -0
  28. iqm/station_control/interface/models/sweep.py +1 -1
  29. iqm/station_control/interface/models/type_aliases.py +7 -1
  30. iqm/station_control/interface/pydantic_base.py +1 -1
  31. iqm/station_control/interface/station_control.py +511 -0
  32. {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/METADATA +2 -2
  33. iqm_station_control_client-9.0.0.dist-info/RECORD +56 -0
  34. iqm/station_control/client/iqm_server/meta_class.py +0 -38
  35. iqm_station_control_client-8.0.0.dist-info/RECORD +0 -54
  36. {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/LICENSE.txt +0 -0
  37. {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/WHEEL +0 -0
  38. {iqm_station_control_client-8.0.0.dist-info → iqm_station_control_client-9.0.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from exa.common.errors.server_errors import StationControlError
15
+ from exa.common.errors.station_control_errors import StationControlError
16
16
 
17
17
 
18
18
  class IqmServerError(StationControlError):
@@ -25,6 +25,7 @@ from pydantic import HttpUrl
25
25
 
26
26
  from iqm.station_control.client.iqm_server import proto
27
27
  from iqm.station_control.client.iqm_server.error import IqmServerError
28
+ from iqm.station_control.interface.models.type_aliases import StrUUID
28
29
 
29
30
 
30
31
  class ClientCallDetails(grpc.ClientCallDetails):
@@ -43,8 +44,7 @@ class ApiTokenAuth(grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInter
43
44
 
44
45
  def _add_auth_header(self, client_call_details) -> ClientCallDetails:
45
46
  details = ClientCallDetails(client_call_details)
46
- token = self.get_token_callback()
47
- details.metadata.append(("authorization", f"Bearer {token}"))
47
+ details.metadata.append(("authorization", self.get_token_callback()))
48
48
  return details
49
49
 
50
50
  def intercept_unary_stream(self, continuation, client_call_details, request):
@@ -115,7 +115,9 @@ def create_channel(
115
115
  return channel
116
116
 
117
117
 
118
- def to_proto_uuid(value: uuid.UUID) -> proto.Uuid:
118
+ def to_proto_uuid(value: StrUUID) -> proto.Uuid:
119
+ if isinstance(value, str):
120
+ value = uuid.UUID(value)
119
121
  return proto.Uuid(raw=value.bytes)
120
122
 
121
123
 
@@ -11,23 +11,28 @@
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
- """StationControlClient implementation for IQM Server"""
14
+ """Client implementation for IQM Server."""
15
15
 
16
- from collections.abc import Callable, Iterable
16
+ from collections.abc import Callable, Iterable, Sequence
17
17
  from contextlib import contextmanager
18
18
  import dataclasses
19
+ from importlib.metadata import version
19
20
  from io import BytesIO
20
21
  import json
21
22
  import logging
23
+ import platform
22
24
  from time import sleep
23
25
  from typing import Any, TypeVar, cast
24
26
  import uuid
27
+ from uuid import UUID
25
28
 
26
29
  import grpc
27
30
  from iqm.models.channel_properties import ChannelProperties
31
+ import requests
28
32
 
29
33
  from exa.common.data.setting_node import SettingNode
30
34
  from exa.common.data.value import ObservationValue, validate_value
35
+ from exa.common.errors.station_control_errors import map_from_status_code_to_error
31
36
  from iqm.station_control.client.iqm_server import proto
32
37
  from iqm.station_control.client.iqm_server.error import IqmServerError
33
38
  from iqm.station_control.client.iqm_server.grpc_utils import (
@@ -39,42 +44,66 @@ from iqm.station_control.client.iqm_server.grpc_utils import (
39
44
  to_datetime,
40
45
  to_proto_uuid,
41
46
  )
42
- from iqm.station_control.client.iqm_server.meta_class import IqmServerClientMeta
43
47
  from iqm.station_control.client.list_models import DutFieldDataList, DutList
44
48
  from iqm.station_control.client.serializers import deserialize_sweep_results, serialize_sweep_job_request
45
49
  from iqm.station_control.client.serializers.channel_property_serializer import unpack_channel_properties
46
50
  from iqm.station_control.client.serializers.setting_node_serializer import deserialize_setting_node
47
51
  from iqm.station_control.client.serializers.task_serializers import deserialize_sweep_job_request
48
- from iqm.station_control.client.station_control import StationControlClient
52
+ from iqm.station_control.interface.list_with_meta import ListWithMeta
49
53
  from iqm.station_control.interface.models import (
50
54
  DutData,
51
55
  DutFieldData,
56
+ DynamicQuantumArchitecture,
57
+ JobData,
52
58
  JobExecutorStatus,
59
+ JobResult,
60
+ ObservationData,
61
+ ObservationDefinition,
62
+ ObservationLite,
63
+ ObservationSetData,
64
+ ObservationSetDefinition,
65
+ ObservationSetUpdate,
66
+ ObservationUpdate,
67
+ QualityMetrics,
68
+ RunData,
69
+ RunDefinition,
70
+ RunLite,
71
+ SequenceMetadataData,
72
+ SequenceMetadataDefinition,
73
+ SequenceResultData,
74
+ SequenceResultDefinition,
75
+ StaticQuantumArchitecture,
53
76
  Statuses,
54
77
  SweepData,
55
78
  SweepDefinition,
56
79
  SweepResults,
57
80
  )
58
- from iqm.station_control.interface.models.jobs import JobData, JobError, JobResult
81
+ from iqm.station_control.interface.models.jobs import JobError
59
82
  from iqm.station_control.interface.models.sweep import SweepBase
83
+ from iqm.station_control.interface.models.type_aliases import GetObservationsMode, SoftwareVersionSet, StrUUID
84
+ from iqm.station_control.interface.station_control import StationControlInterface
60
85
 
61
86
  logger = logging.getLogger(__name__)
62
87
 
63
88
  T = TypeVar("T")
64
89
 
65
90
 
66
- class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
91
+ class IqmServerClient(StationControlInterface):
67
92
  def __init__(
68
93
  self,
69
94
  root_url: str,
95
+ *,
70
96
  get_token_callback: Callable[[], str] | None = None,
97
+ client_signature: str | None = None,
71
98
  grpc_channel: grpc.Channel | None = None,
72
99
  ):
73
100
  self.root_url = root_url
74
101
  self._connection_params = parse_connection_params(root_url)
75
102
  self._cached_resources = {}
76
103
  self._latest_submitted_sweep = None
77
- self._channel = grpc_channel or create_channel(self._connection_params, get_token_callback)
104
+ self._get_token_callback = get_token_callback
105
+ self._signature = self._create_signature(client_signature)
106
+ self._channel = grpc_channel or create_channel(self._connection_params, self._get_token_callback)
78
107
  self._current_qc = resolve_current_qc(self._channel, self._connection_params.quantum_computer)
79
108
 
80
109
  def __del__(self):
@@ -86,9 +115,18 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
86
115
  def get_about(self) -> dict:
87
116
  return self._get_resource("about", parse_json)
88
117
 
118
+ def get_health(self) -> dict:
119
+ raise NotImplementedError
120
+
89
121
  def get_configuration(self) -> dict:
90
122
  return self._get_resource("configuration", parse_json)
91
123
 
124
+ def get_exa_configuration(self) -> str:
125
+ raise NotImplementedError
126
+
127
+ def get_or_create_software_version_set(self, software_version_set: SoftwareVersionSet) -> int:
128
+ raise NotImplementedError
129
+
92
130
  def get_settings(self) -> SettingNode:
93
131
  return self._get_resource("settings", deserialize_setting_node).copy()
94
132
 
@@ -98,14 +136,6 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
98
136
  def get_channel_properties(self) -> dict[str, ChannelProperties]:
99
137
  return self._get_resource("channel-properties", unpack_channel_properties)
100
138
 
101
- def get_duts(self) -> list[DutData]:
102
- return self._get_resource("duts", lambda data: DutList.model_validate(parse_json(data)))
103
-
104
- def get_dut_fields(self, dut_label: str) -> list[DutFieldData]:
105
- return self._get_resource(
106
- f"dut-fields/{dut_label}", lambda data: DutFieldDataList.model_validate(parse_json(data))
107
- )
108
-
109
139
  def sweep(self, sweep_definition: SweepDefinition) -> dict:
110
140
  with wrap_error("Job submission failed"):
111
141
  jobs = proto.JobsStub(self._channel)
@@ -126,8 +156,11 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
126
156
  "job_id": str(job_id),
127
157
  }
128
158
 
129
- def get_sweep(self, sweep_id: uuid.UUID) -> SweepData:
159
+ def get_sweep(self, sweep_id: UUID) -> SweepData:
130
160
  with wrap_error("Job loading failed"):
161
+ if isinstance(sweep_id, str):
162
+ sweep_id = uuid.UUID(sweep_id)
163
+
131
164
  jobs = proto.JobsStub(self._channel)
132
165
  job_lookup = proto.JobLookupV1(id=to_proto_uuid(sweep_id))
133
166
  job: proto.JobV1 = jobs.GetJobV1(job_lookup)
@@ -146,18 +179,118 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
146
179
  **{f.name: getattr(sweep, f.name) for f in dataclasses.fields(SweepBase)},
147
180
  )
148
181
 
149
- def get_sweep_results(self, sweep_id: uuid.UUID) -> SweepResults:
182
+ def delete_sweep(self, sweep_id: UUID) -> None:
183
+ raise NotImplementedError
184
+
185
+ def get_sweep_results(self, sweep_id: UUID) -> SweepResults:
150
186
  with wrap_error("Job result loading failed"):
151
187
  jobs = proto.JobsStub(self._channel)
152
188
  data_chunks = jobs.GetJobResultsV1(proto.JobLookupV1(id=to_proto_uuid(sweep_id)))
153
189
  return deserialize_sweep_results(load_all(data_chunks))
154
190
 
155
- def abort_job(self, sweep_id: uuid.UUID) -> None:
156
- with wrap_error("Job cancellation failed"):
157
- jobs = proto.JobsStub(self._channel)
158
- jobs.CancelJobV1(proto.JobLookupV1(id=to_proto_uuid(sweep_id)))
191
+ def run(
192
+ self,
193
+ run_definition: RunDefinition,
194
+ update_progress_callback: Callable[[Statuses], None] | None = None,
195
+ wait_job_completion: bool = True,
196
+ ) -> bool:
197
+ raise NotImplementedError
198
+
199
+ def get_run(self, run_id: UUID) -> RunData:
200
+ raise NotImplementedError
201
+
202
+ def query_runs(self, **kwargs) -> ListWithMeta[RunLite]:
203
+ raise NotImplementedError
204
+
205
+ def create_observations(
206
+ self, observation_definitions: Sequence[ObservationDefinition]
207
+ ) -> ListWithMeta[ObservationData]:
208
+ raise NotImplementedError
209
+
210
+ def get_observations(
211
+ self,
212
+ *,
213
+ mode: GetObservationsMode,
214
+ dut_label: str | None = None,
215
+ dut_field: str | None = None,
216
+ tags: list[str] | None = None,
217
+ invalid: bool | None = False,
218
+ run_ids: list[UUID] | None = None,
219
+ sequence_ids: list[UUID] | None = None,
220
+ limit: int | None = None,
221
+ ) -> list[ObservationData]:
222
+ raise NotImplementedError
223
+
224
+ def query_observations(self, **kwargs) -> ListWithMeta[ObservationData]:
225
+ raise NotImplementedError
226
+
227
+ def update_observations(self, observation_updates: Sequence[ObservationUpdate]) -> list[ObservationData]:
228
+ raise NotImplementedError
229
+
230
+ def query_observation_sets(self, **kwargs) -> ListWithMeta[ObservationSetData]:
231
+ raise NotImplementedError
232
+
233
+ def create_observation_set(self, observation_set_definition: ObservationSetDefinition) -> ObservationSetData:
234
+ raise NotImplementedError
159
235
 
160
- def get_job(self, job_id: uuid.UUID) -> JobData:
236
+ def get_observation_set(self, observation_set_id: UUID) -> ObservationSetData:
237
+ raise NotImplementedError
238
+
239
+ def update_observation_set(self, observation_set_update: ObservationSetUpdate) -> ObservationSetData:
240
+ raise NotImplementedError
241
+
242
+ def finalize_observation_set(self, observation_set_id: UUID) -> None:
243
+ raise NotImplementedError
244
+
245
+ def get_observation_set_observations(self, observation_set_id: UUID) -> list[ObservationLite]:
246
+ raise NotImplementedError
247
+
248
+ def get_default_calibration_set(self) -> ObservationSetData:
249
+ raise NotImplementedError
250
+
251
+ def get_default_calibration_set_observations(self) -> list[ObservationLite]:
252
+ raise NotImplementedError
253
+
254
+ def get_dynamic_quantum_architecture(self, calibration_set_id: UUID) -> DynamicQuantumArchitecture:
255
+ response = self._send_request(requests.get, f"api/v1/calibration/{calibration_set_id}/gates")
256
+ return DynamicQuantumArchitecture.model_validate_json(response.text)
257
+
258
+ def get_default_dynamic_quantum_architecture(self) -> DynamicQuantumArchitecture:
259
+ raise NotImplementedError
260
+
261
+ def get_default_calibration_set_quality_metrics(self) -> QualityMetrics:
262
+ raise NotImplementedError
263
+
264
+ def get_calibration_set_quality_metrics(self, calibration_set_id: UUID) -> QualityMetrics:
265
+ raise NotImplementedError
266
+
267
+ def get_duts(self) -> list[DutData]:
268
+ return self._get_resource("duts", lambda data: DutList.model_validate(parse_json(data)))
269
+
270
+ def get_dut_fields(self, dut_label: str) -> list[DutFieldData]:
271
+ return self._get_resource(
272
+ f"dut-fields/{dut_label}", lambda data: DutFieldDataList.model_validate(parse_json(data))
273
+ )
274
+
275
+ def query_sequence_metadatas(self, **kwargs) -> ListWithMeta[SequenceMetadataData]:
276
+ raise NotImplementedError
277
+
278
+ def create_sequence_metadata(
279
+ self, sequence_metadata_definition: SequenceMetadataDefinition
280
+ ) -> SequenceMetadataData:
281
+ raise NotImplementedError
282
+
283
+ def save_sequence_result(self, sequence_result_definition: SequenceResultDefinition) -> SequenceResultData:
284
+ raise NotImplementedError
285
+
286
+ def get_sequence_result(self, sequence_id: UUID) -> SequenceResultData:
287
+ raise NotImplementedError
288
+
289
+ def get_static_quantum_architecture(self, dut_label: str) -> StaticQuantumArchitecture:
290
+ response = self._send_request(requests.get, "api/v1/quantum-architecture")
291
+ return StaticQuantumArchitecture.model_validate_json(response.text)
292
+
293
+ def get_job(self, job_id: StrUUID) -> JobData:
161
294
  with wrap_error("Job loading failed"):
162
295
  jobs = proto.JobsStub(self._channel)
163
296
  job: proto.JobV1 = jobs.GetJobV1(proto.JobLookupV1(id=to_proto_uuid(job_id)))
@@ -175,6 +308,34 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
175
308
  position=job.queue_position if job.HasField("queue_position") else None,
176
309
  )
177
310
 
311
+ def abort_job(self, job_id: StrUUID) -> None:
312
+ with wrap_error("Job cancellation failed"):
313
+ jobs = proto.JobsStub(self._channel)
314
+ jobs.CancelJobV1(proto.JobLookupV1(id=to_proto_uuid(job_id)))
315
+
316
+ def get_calibration_set_values(self, calibration_set_id: StrUUID) -> dict[str, ObservationValue]:
317
+ with wrap_error("Calibration set loading failed"):
318
+ calibrations = proto.CalibrationsStub(self._channel)
319
+ data_chunks = calibrations.GetFullCalibrationDataV1(
320
+ proto.CalibrationLookupV1(
321
+ id=to_proto_uuid(calibration_set_id),
322
+ )
323
+ )
324
+ _, cal_set_values = parse_calibration_set(load_all(data_chunks))
325
+ return cal_set_values
326
+
327
+ def get_latest_calibration_set_id(self, dut_label: str) -> uuid.UUID:
328
+ with wrap_error("Calibration set metadata loading failed"):
329
+ calibrations = proto.CalibrationsStub(self._channel)
330
+ metadata: proto.CalibrationMetadataV1 = calibrations.GetLatestQuantumComputerCalibrationV1(
331
+ proto.LatestQuantumComputerCalibrationLookupV1(
332
+ qc_id=self._current_qc.id,
333
+ )
334
+ )
335
+ if metadata.dut_label != dut_label:
336
+ raise ValueError(f"No calibration set for dut_label = {dut_label}")
337
+ return from_proto_uuid(metadata.id)
338
+
178
339
  def _wait_job_completion(
179
340
  self,
180
341
  task_id: str,
@@ -202,29 +363,6 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
202
363
  except KeyboardInterrupt:
203
364
  return True
204
365
 
205
- def get_calibration_set_values(self, calibration_set_id: uuid.UUID) -> dict[str, ObservationValue]:
206
- with wrap_error("Calibration set loading failed"):
207
- calibrations = proto.CalibrationsStub(self._channel)
208
- data_chunks = calibrations.GetFullCalibrationDataV1(
209
- proto.CalibrationLookupV1(
210
- id=to_proto_uuid(calibration_set_id),
211
- )
212
- )
213
- _, cal_set_values = parse_calibration_set(load_all(data_chunks))
214
- return cal_set_values
215
-
216
- def get_latest_calibration_set_id(self, dut_label: str) -> uuid.UUID:
217
- with wrap_error("Calibration set metadata loading failed"):
218
- calibrations = proto.CalibrationsStub(self._channel)
219
- metadata: proto.CalibrationMetadataV1 = calibrations.GetLatestQuantumComputerCalibrationV1(
220
- proto.LatestQuantumComputerCalibrationLookupV1(
221
- qc_id=self._current_qc.id,
222
- )
223
- )
224
- if metadata.dut_label != dut_label:
225
- raise ValueError(f"No calibration set for dut_label = {dut_label}")
226
- return from_proto_uuid(metadata.id)
227
-
228
366
  def _get_cached_sweep(self, sweep_id: uuid.UUID) -> SweepDefinition | None:
229
367
  latest_submitted = self._latest_submitted_sweep
230
368
  if latest_submitted and latest_submitted.sweep_id == sweep_id:
@@ -246,6 +384,94 @@ class IqmServerClient(StationControlClient, metaclass=IqmServerClientMeta):
246
384
  self._cached_resources[resource_name] = resource
247
385
  return resource
248
386
 
387
+ @staticmethod
388
+ def _create_signature(client_signature: str) -> str:
389
+ signature = f"{platform.platform(terse=True)}"
390
+ signature += f", python {platform.python_version()}"
391
+ version_string = "iqm-station-control-client"
392
+ signature += f", IqmServerClient {version_string} {version(version_string)}"
393
+ if client_signature:
394
+ signature += f", {client_signature}"
395
+ return signature
396
+
397
+ def _send_request(
398
+ self,
399
+ http_method: Callable[..., requests.Response],
400
+ url_path: str,
401
+ *,
402
+ json_str: str | None = None,
403
+ octets: bytes | None = None,
404
+ params: dict[str, Any] | None = None,
405
+ headers: dict[str, str] | None = None,
406
+ timeout: int = 120,
407
+ ) -> requests.Response:
408
+ """Send an HTTP request.
409
+
410
+ Parameters ``json_str``, ``octets`` and ``params`` are mutually exclusive.
411
+ The first non-None argument (in this order) will be used to construct the body of the request.
412
+
413
+ Args:
414
+ http_method: HTTP method to use for the request, any of requests.[post|get|put|head|delete|patch|options].
415
+ url_path: URL for the request.
416
+
417
+ Returns:
418
+ Response to the request.
419
+
420
+ Raises:
421
+ StationControlError: Request was not successful.
422
+
423
+ """
424
+ # Will raise an error if respectively an error response code is returned.
425
+ # http_method should be any of requests.[post|get|put|head|delete|patch|options]
426
+
427
+ request_kwargs = self._build_request_kwargs(
428
+ json_str=json_str, octets=octets, params=params or {}, headers=headers or {}, timeout=timeout
429
+ )
430
+ url = f"{self.root_url}/{url_path}"
431
+ response = http_method(url, **request_kwargs)
432
+ if not response.ok:
433
+ try:
434
+ response_json = response.json()
435
+ error_message = response_json["detail"]
436
+ except json.JSONDecodeError:
437
+ error_message = response.text
438
+
439
+ try:
440
+ error_class = map_from_status_code_to_error(response.status_code)
441
+ except KeyError:
442
+ raise RuntimeError(f"Unexpected response status code {response.status_code}: {error_message}")
443
+ raise error_class(error_message)
444
+ return response
445
+
446
+ def _build_request_kwargs(self, **kwargs):
447
+ kwargs.setdefault("headers", {"User-Agent": self._signature})
448
+
449
+ params = kwargs.get("params", {})
450
+ headers = kwargs["headers"]
451
+
452
+ if kwargs["json_str"] is not None:
453
+ # Must be able to handle JSON strings with arbitrary unicode characters, so we use an explicit
454
+ # encoding into bytes, and set the headers so the recipient can decode the request body correctly.
455
+ data = kwargs["json_str"].encode("utf-8")
456
+ headers["Content-Type"] = "application/json; charset=UTF-8"
457
+ elif kwargs["octets"] is not None:
458
+ data = kwargs["octets"]
459
+ headers["Content-Type"] = "application/octet-stream"
460
+ else:
461
+ data = None
462
+
463
+ # If token callback exists, use it to retrieve the token and add it to the headers
464
+ if self._get_token_callback:
465
+ headers["Authorization"] = self._get_token_callback()
466
+
467
+ kwargs = {
468
+ "params": params,
469
+ "data": data,
470
+ "headers": headers,
471
+ "timeout": kwargs["timeout"],
472
+ }
473
+ return _remove_empty_values(kwargs)
474
+
249
475
 
250
476
  def resolve_current_qc(channel: grpc.Channel, alias: str) -> proto.QuantumComputerV1:
251
477
  qcs = proto.QuantumComputersStub(channel)
@@ -334,3 +560,8 @@ def wrap_error(title: str):
334
560
  raise extract_error(e, title) from e
335
561
  except Exception as e:
336
562
  raise IqmServerError(message=f"{title}: {e}", status_code=str(grpc.StatusCode.INTERNAL.name)) from e
563
+
564
+
565
+ def _remove_empty_values(kwargs):
566
+ # Remove None and {} values
567
+ return {key: value for key, value in kwargs.items() if value not in [None, {}]}
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -11,9 +11,12 @@
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
- """Station control client type adapters."""
14
+ """Station control client list types for different models.
15
15
 
16
- from typing import Generic, TypeVar
16
+ These are used mainly for easy serialization and deserialization of list of objects.
17
+ """
18
+
19
+ from typing import Generic, TypeAlias, TypeVar
17
20
 
18
21
  from pydantic import ConfigDict, RootModel
19
22
 
@@ -53,17 +56,20 @@ class ListModel(RootModel):
53
56
  def __len__(self) -> int:
54
57
  return len(self.root)
55
58
 
59
+ def __str__(self) -> str:
60
+ return str(self.root)
61
+
56
62
  model_config = ConfigDict(
57
63
  ser_json_inf_nan="constants", # Will serialize Infinity and NaN values as Infinity and NaN
58
64
  )
59
65
 
60
66
 
61
- DutList = ListModel[list[DutData]] # type: ignore
62
- DutFieldDataList = ListModel[list[DutFieldData]] # type: ignore
63
- ObservationDataList = ListModel[list[ObservationData]] # type: ignore
64
- ObservationDefinitionList = ListModel[list[ObservationDefinition]] # type: ignore
65
- ObservationLiteList = ListModel[list[ObservationLite]] # type: ignore
66
- ObservationUpdateList = ListModel[list[ObservationUpdate]] # type: ignore
67
- ObservationSetDataList = ListModel[list[ObservationSetData]] # type: ignore
68
- SequenceMetadataDataList = ListModel[list[SequenceMetadataData]] # type: ignore
69
- RunLiteList = ListModel[list[RunLite]] # type: ignore
67
+ DutList: TypeAlias = ListModel[list[DutData]]
68
+ DutFieldDataList: TypeAlias = ListModel[list[DutFieldData]]
69
+ ObservationDataList: TypeAlias = ListModel[list[ObservationData]]
70
+ ObservationDefinitionList: TypeAlias = ListModel[list[ObservationDefinition]]
71
+ ObservationLiteList: TypeAlias = ListModel[list[ObservationLite]]
72
+ ObservationUpdateList: TypeAlias = ListModel[list[ObservationUpdate]]
73
+ ObservationSetDataList: TypeAlias = ListModel[list[ObservationSetData]]
74
+ SequenceMetadataDataList: TypeAlias = ListModel[list[SequenceMetadataData]]
75
+ RunLiteList: TypeAlias = ListModel[list[RunLite]]
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,10 +1,16 @@
1
- # ********************************************************************************
2
- # Copyright (c) 2019-2024 IQM Finland Oy.
3
- # All rights reserved. Confidential and proprietary.
1
+ # Copyright 2025 IQM
4
2
  #
5
- # Distribution or reproduction of any information contained herein
6
- # is prohibited without IQM Finland Oy’s prior written permission.
7
- # ********************************************************************************
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
8
14
  """Serializers and deserializers for :class:`~iqm.models.channel_properties.channel_properties.ChannelProperties`"""
9
15
 
10
16
  from collections.abc import Iterable
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -27,8 +27,7 @@ from exa.common.data.setting_node import SettingNode
27
27
  from exa.common.sweep.database_serialization import decode_and_validate_sweeps, encode_nd_sweeps
28
28
  from iqm.station_control.client.serializers.datetime_serializers import deserialize_datetime, serialize_datetime
29
29
  from iqm.station_control.client.serializers.playlist_serializers import pack_playlist, unpack_playlist
30
- from iqm.station_control.interface.models import SweepData, SweepDefinition, SweepResults
31
- from iqm.station_control.interface.models.jobs import JobExecutorStatus
30
+ from iqm.station_control.interface.models import JobExecutorStatus, SweepData, SweepDefinition, SweepResults
32
31
 
33
32
 
34
33
  def serialize_sweep_definition(sweep_definition: SweepDefinition) -> SweepDefinitionProto:
@@ -1,4 +1,4 @@
1
- # Copyright 2024 IQM
1
+ # Copyright 2025 IQM
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.