iqm-station-control-client 9.0.0__tar.gz → 9.2.0__tar.gz

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 (81) hide show
  1. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/CHANGELOG.rst +16 -0
  2. {iqm_station_control_client-9.0.0/src/iqm_station_control_client.egg-info → iqm_station_control_client-9.2.0}/PKG-INFO +1 -1
  3. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/iqm_server_client.py +4 -102
  4. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/uuid_pb2.pyi +1 -1
  5. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/list_models.py +9 -9
  6. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/station_control.py +146 -107
  7. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0/src/iqm_station_control_client.egg-info}/PKG-INFO +1 -1
  8. iqm_station_control_client-9.2.0/version.txt +1 -0
  9. iqm_station_control_client-9.0.0/version.txt +0 -1
  10. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/LICENSE.txt +0 -0
  11. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/MANIFEST.in +0 -0
  12. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/README.rst +0 -0
  13. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/API.rst +0 -0
  14. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/Makefile +0 -0
  15. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/_static/css/custom.css +0 -0
  16. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/_static/images/favicon.ico +0 -0
  17. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/_static/images/logo.png +0 -0
  18. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/_templates/autosummary-class-template.rst +0 -0
  19. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/_templates/autosummary-module-template.rst +0 -0
  20. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/changelog.rst +0 -0
  21. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/conf.py +0 -0
  22. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/index.rst +0 -0
  23. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/docs/license.rst +0 -0
  24. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/pyproject.toml +0 -0
  25. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/requirements/base.in +0 -0
  26. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/requirements/base.txt +0 -0
  27. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/setup.cfg +0 -0
  28. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/setup.py +0 -0
  29. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/__init__.py +0 -0
  30. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/__init__.py +0 -0
  31. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/error.py +0 -0
  32. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/grpc_utils.py +0 -0
  33. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/__init__.py +0 -0
  34. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/calibration_pb2.py +0 -0
  35. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/calibration_pb2.pyi +0 -0
  36. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/calibration_pb2_grpc.py +0 -0
  37. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/common_pb2.py +0 -0
  38. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/common_pb2.pyi +0 -0
  39. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/common_pb2_grpc.py +0 -0
  40. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/job_pb2.py +0 -0
  41. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/job_pb2.pyi +0 -0
  42. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/job_pb2_grpc.py +0 -0
  43. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/qc_pb2.py +0 -0
  44. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/qc_pb2.pyi +0 -0
  45. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/qc_pb2_grpc.py +0 -0
  46. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/uuid_pb2.py +0 -0
  47. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/proto/uuid_pb2_grpc.py +0 -0
  48. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/testing/__init__.py +0 -0
  49. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/iqm_server/testing/iqm_server_mock.py +0 -0
  50. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/__init__.py +0 -0
  51. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/channel_property_serializer.py +0 -0
  52. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/datetime_serializers.py +0 -0
  53. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/playlist_serializers.py +0 -0
  54. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/run_serializers.py +0 -0
  55. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/setting_node_serializer.py +0 -0
  56. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/struct_serializer.py +0 -0
  57. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/sweep_serializers.py +0 -0
  58. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/serializers/task_serializers.py +0 -0
  59. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/client/utils.py +0 -0
  60. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/__init__.py +0 -0
  61. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/list_with_meta.py +0 -0
  62. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/__init__.py +0 -0
  63. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/dut.py +0 -0
  64. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/dynamic_quantum_architecture.py +0 -0
  65. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/jobs.py +0 -0
  66. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/monitor.py +0 -0
  67. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/observation.py +0 -0
  68. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/observation_set.py +0 -0
  69. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/run.py +0 -0
  70. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/sequence.py +0 -0
  71. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/static_quantum_architecture.py +0 -0
  72. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/sweep.py +0 -0
  73. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/models/type_aliases.py +0 -0
  74. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/pydantic_base.py +0 -0
  75. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm/station_control/interface/station_control.py +0 -0
  76. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm_station_control_client.egg-info/SOURCES.txt +0 -0
  77. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm_station_control_client.egg-info/dependency_links.txt +0 -0
  78. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm_station_control_client.egg-info/requires.txt +0 -0
  79. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/src/iqm_station_control_client.egg-info/top_level.txt +0 -0
  80. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/tests/.pylintrc +0 -0
  81. {iqm_station_control_client-9.0.0 → iqm_station_control_client-9.2.0}/tests/__init__.py +0 -0
@@ -2,6 +2,22 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 9.2.0 (2025-06-25)
6
+ ==========================
7
+
8
+ Bug fixes
9
+ ---------
10
+
11
+ - Fix failing type checks in mypy.
12
+
13
+ Version 9.1.0 (2025-06-24)
14
+ ==========================
15
+
16
+ Bug fixes
17
+ ---------
18
+
19
+ - Requests now always have User-Agent header.
20
+
5
21
  Version 9.0.0 (2025-06-13)
6
22
  ==========================
7
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-station-control-client
3
- Version: 9.0.0
3
+ Version: 9.2.0
4
4
  Summary: Python client for communicating with Station Control Service
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -16,11 +16,9 @@
16
16
  from collections.abc import Callable, Iterable, Sequence
17
17
  from contextlib import contextmanager
18
18
  import dataclasses
19
- from importlib.metadata import version
20
19
  from io import BytesIO
21
20
  import json
22
21
  import logging
23
- import platform
24
22
  from time import sleep
25
23
  from typing import Any, TypeVar, cast
26
24
  import uuid
@@ -32,7 +30,6 @@ import requests
32
30
 
33
31
  from exa.common.data.setting_node import SettingNode
34
32
  from exa.common.data.value import ObservationValue, validate_value
35
- from exa.common.errors.station_control_errors import map_from_status_code_to_error
36
33
  from iqm.station_control.client.iqm_server import proto
37
34
  from iqm.station_control.client.iqm_server.error import IqmServerError
38
35
  from iqm.station_control.client.iqm_server.grpc_utils import (
@@ -49,6 +46,7 @@ from iqm.station_control.client.serializers import deserialize_sweep_results, se
49
46
  from iqm.station_control.client.serializers.channel_property_serializer import unpack_channel_properties
50
47
  from iqm.station_control.client.serializers.setting_node_serializer import deserialize_setting_node
51
48
  from iqm.station_control.client.serializers.task_serializers import deserialize_sweep_job_request
49
+ from iqm.station_control.client.station_control import _StationControlClientBase
52
50
  from iqm.station_control.interface.list_with_meta import ListWithMeta
53
51
  from iqm.station_control.interface.models import (
54
52
  DutData,
@@ -81,14 +79,13 @@ from iqm.station_control.interface.models import (
81
79
  from iqm.station_control.interface.models.jobs import JobError
82
80
  from iqm.station_control.interface.models.sweep import SweepBase
83
81
  from iqm.station_control.interface.models.type_aliases import GetObservationsMode, SoftwareVersionSet, StrUUID
84
- from iqm.station_control.interface.station_control import StationControlInterface
85
82
 
86
83
  logger = logging.getLogger(__name__)
87
84
 
88
85
  T = TypeVar("T")
89
86
 
90
87
 
91
- class IqmServerClient(StationControlInterface):
88
+ class IqmServerClient(_StationControlClientBase):
92
89
  def __init__(
93
90
  self,
94
91
  root_url: str,
@@ -97,12 +94,10 @@ class IqmServerClient(StationControlInterface):
97
94
  client_signature: str | None = None,
98
95
  grpc_channel: grpc.Channel | None = None,
99
96
  ):
100
- self.root_url = root_url
97
+ super().__init__(root_url, get_token_callback=get_token_callback, client_signature=client_signature)
101
98
  self._connection_params = parse_connection_params(root_url)
102
- self._cached_resources = {}
99
+ self._cached_resources: dict[str, Any] = {}
103
100
  self._latest_submitted_sweep = None
104
- self._get_token_callback = get_token_callback
105
- self._signature = self._create_signature(client_signature)
106
101
  self._channel = grpc_channel or create_channel(self._connection_params, self._get_token_callback)
107
102
  self._current_qc = resolve_current_qc(self._channel, self._connection_params.quantum_computer)
108
103
 
@@ -384,94 +379,6 @@ class IqmServerClient(StationControlInterface):
384
379
  self._cached_resources[resource_name] = resource
385
380
  return resource
386
381
 
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
-
475
382
 
476
383
  def resolve_current_qc(channel: grpc.Channel, alias: str) -> proto.QuantumComputerV1:
477
384
  qcs = proto.QuantumComputersStub(channel)
@@ -560,8 +467,3 @@ def wrap_error(title: str):
560
467
  raise extract_error(e, title) from e
561
468
  except Exception as e:
562
469
  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, {}]}
@@ -23,4 +23,4 @@ class Uuid(_message.Message):
23
23
  STR_FIELD_NUMBER: _ClassVar[int]
24
24
  raw: bytes
25
25
  str: str
26
- def __init__(self, raw: _Optional[bytes] = ..., str: _Optional[str] = ...) -> None: ...
26
+ def __init__(self, raw: _Optional[bytes] = ..., str: _Optional[str] = ...) -> None: ...# type: ignore[valid-type]
@@ -64,12 +64,12 @@ class ListModel(RootModel):
64
64
  )
65
65
 
66
66
 
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]]
67
+ DutList: TypeAlias = ListModel[list[DutData]] # type: ignore[type-arg]
68
+ DutFieldDataList: TypeAlias = ListModel[list[DutFieldData]] # type: ignore[type-arg]
69
+ ObservationDataList: TypeAlias = ListModel[list[ObservationData]] # type: ignore[type-arg]
70
+ ObservationDefinitionList: TypeAlias = ListModel[list[ObservationDefinition]] # type: ignore[type-arg]
71
+ ObservationLiteList: TypeAlias = ListModel[list[ObservationLite]] # type: ignore[type-arg]
72
+ ObservationUpdateList: TypeAlias = ListModel[list[ObservationUpdate]] # type: ignore[type-arg]
73
+ ObservationSetDataList: TypeAlias = ListModel[list[ObservationSetData]] # type: ignore[type-arg]
74
+ SequenceMetadataDataList: TypeAlias = ListModel[list[SequenceMetadataData]] # type: ignore[type-arg]
75
+ RunLiteList: TypeAlias = ListModel[list[RunLite]] # type: ignore[type-arg]
@@ -100,23 +100,160 @@ logger = logging.getLogger(__name__)
100
100
  TypePydanticBase = TypeVar("TypePydanticBase", bound=PydanticBase)
101
101
 
102
102
 
103
- class StationControlClient(StationControlInterface):
103
+ class _StationControlClientBase(StationControlInterface):
104
+ """Shared functionality for StationControlClient and IqmServerClient.
105
+
106
+ Args:
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.
113
+
114
+ """
115
+
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
+ ):
124
+ self.root_url = root_url
125
+ self._get_token_callback = get_token_callback
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
138
+
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.
151
+
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.
154
+
155
+ Args:
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.
168
+
169
+ """
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}")
191
+
192
+ raise error_class(error_message)
193
+ return response
194
+
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"
218
+
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):
104
237
  """Client implementation for station control service REST API.
105
238
 
106
239
  Args:
107
240
  root_url: Remote station control service URL.
108
241
  get_token_callback: A callback function that returns a token (str)
109
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.
110
245
 
111
246
  """
112
247
 
113
248
  def __init__(
114
249
  self, root_url: str, *, get_token_callback: Callable[[], str] | None = None, client_signature: str | None = None
115
250
  ):
116
- self.root_url = root_url
117
- self._enable_opentelemetry = os.environ.get("JAEGER_OPENTELEMETRY_COLLECTOR_ENDPOINT", None) is not None
118
- self._get_token_callback = get_token_callback
119
- self._signature = self._create_signature(client_signature)
251
+ super().__init__(
252
+ root_url,
253
+ get_token_callback=get_token_callback,
254
+ client_signature=client_signature, # type: ignore[arg-type]
255
+ enable_opentelemetry=os.environ.get("JAEGER_OPENTELEMETRY_COLLECTOR_ENDPOINT", None) is not None,
256
+ )
120
257
  # TODO SW-1387: Remove this when using v1 API, not needed
121
258
  self._check_api_versions()
122
259
  qcm_data_url = os.environ.get("CHIP_DESIGN_RECORD_FALLBACK_URL", None)
@@ -358,16 +495,6 @@ class StationControlClient(StationControlInterface):
358
495
  def abort_job(self, job_id: StrUUID) -> None:
359
496
  self._send_request(requests.post, f"jobs/{job_id}/abort")
360
497
 
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
370
-
371
498
  def _wait_job_completion(self, job_id: str, update_progress_callback: Callable[[Statuses], None] | None) -> bool:
372
499
  logger.info("Waiting for job ID: %s", job_id)
373
500
  update_progress_callback = update_progress_callback or (lambda status: None)
@@ -438,62 +565,6 @@ class StationControlClient(StationControlInterface):
438
565
  # using the \uXXXX syntax, BaseModel.model_dump_json() keeps them in the produced JSON str.
439
566
  return model.model_dump_json()
440
567
 
441
- def _send_request(
442
- self,
443
- http_method: Callable[..., requests.Response],
444
- url_path: str,
445
- *,
446
- json_str: str | None = None,
447
- octets: bytes | None = None,
448
- params: dict[str, Any] | None = None,
449
- headers: dict[str, str] | None = None,
450
- timeout: int = 120,
451
- ) -> requests.Response:
452
- """Send an HTTP request.
453
-
454
- Parameters ``json_str``, ``octets`` and ``params`` are mutually exclusive.
455
- The first non-None argument (in this order) will be used to construct the body of the request.
456
-
457
- Args:
458
- http_method: HTTP method to use for the request, any of requests.[post|get|put|head|delete|patch|options].
459
- url_path: URL for the request.
460
- json_str: JSON string to store in the body, may contain arbitrary Unicode characters.
461
- octets: Pre-serialized binary data to store in the body.
462
- params: HTTP query to store in the body.
463
- headers: Additional HTTP headers for the request. Some may be overridden.
464
-
465
- Returns:
466
- Response to the request.
467
-
468
- Raises:
469
- StationControlError: Request was not successful.
470
-
471
- """
472
- # Will raise an error if respectively an error response code is returned.
473
- # http_method should be any of requests.[post|get|put|head|delete|patch|options]
474
-
475
- request_kwargs = self._build_request_kwargs(
476
- json_str=json_str, octets=octets, params=params or {}, headers=headers or {}, timeout=timeout
477
- )
478
- url = f"{self.root_url}/{url_path}"
479
- # TODO SW-1387: Use v1 API
480
- # url = f"{self.root_url}/{self.version}/{url_path}"
481
- response = http_method(url, **request_kwargs)
482
- if not response.ok:
483
- try:
484
- response_json = response.json()
485
- error_message = response_json["detail"]
486
- except json.JSONDecodeError:
487
- error_message = response.text
488
-
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
-
494
- raise error_class(error_message)
495
- return response
496
-
497
568
  # TODO SW-1387: Remove this when using v1 API, not needed
498
569
  def _check_api_versions(self):
499
570
  client_api_version = self._get_client_api_version()
@@ -529,41 +600,9 @@ class StationControlClient(StationControlInterface):
529
600
  def _get_client_api_version() -> Version:
530
601
  return parse(version("iqm-station-control-client"))
531
602
 
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
-
564
603
  @staticmethod
565
604
  def _clean_query_parameters(model: Any, **kwargs) -> dict[str, Any]:
566
- if issubclass(model, PydanticBase) and "invalid" in model.model_fields.keys() and "invalid" not in kwargs:
605
+ if issubclass(model, PydanticBase) and "invalid" in model.model_fields and "invalid" not in kwargs:
567
606
  # Get only valid items by default, "invalid=None" would return also invalid ones.
568
607
  # This default has to be set on the client side, server side uses default "None".
569
608
  kwargs["invalid"] = False
@@ -572,7 +611,7 @@ class StationControlClient(StationControlInterface):
572
611
  @staticmethod
573
612
  def _deserialize_response(
574
613
  response: requests.Response,
575
- model_class: type[TypePydanticBase] | type[ListModel[list[TypePydanticBase]]],
614
+ model_class: type[TypePydanticBase | ListModel[list[TypePydanticBase]]],
576
615
  *,
577
616
  list_with_meta: bool = False,
578
617
  ) -> TypePydanticBase | ListWithMeta[TypePydanticBase]:
@@ -590,6 +629,6 @@ class StationControlClient(StationControlInterface):
590
629
  return model
591
630
 
592
631
 
593
- def _remove_empty_values(kwargs):
594
- # Remove None and {} values
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 {}."""
595
634
  return {key: value for key, value in kwargs.items() if value not in [None, {}]}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-station-control-client
3
- Version: 9.0.0
3
+ Version: 9.2.0
4
4
  Summary: Python client for communicating with Station Control Service
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -0,0 +1 @@
1
+ 9.2.0
@@ -1 +0,0 @@
1
- 9.0.0