iqm-station-control-client 11.3.1__tar.gz → 12.0.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 (93) hide show
  1. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/CHANGELOG.rst +34 -8
  2. iqm_station_control_client-12.0.0/MANIFEST.in +4 -0
  3. {iqm_station_control_client-11.3.1/src/iqm_station_control_client.egg-info → iqm_station_control_client-12.0.0}/PKG-INFO +3 -3
  4. iqm_station_control_client-12.0.0/docs/changelog.rst +8 -0
  5. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/pyproject.toml +4 -5
  6. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/requirements/base.txt +2 -2
  7. iqm_station_control_client-12.0.0/src/iqm/station_control/client/authentication.py +239 -0
  8. iqm_station_control_client-12.0.0/src/iqm/station_control/client/iqm_server/iqm_server_client.py +0 -0
  9. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/list_models.py +16 -11
  10. iqm_station_control_client-12.0.0/src/iqm/station_control/client/py.typed +0 -0
  11. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/qon.py +1 -1
  12. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/run_serializers.py +5 -4
  13. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/struct_serializer.py +1 -1
  14. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/station_control.py +140 -154
  15. iqm_station_control_client-12.0.0/src/iqm/station_control/client/utils.py +33 -0
  16. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/__init__.py +21 -2
  17. iqm_station_control_client-12.0.0/src/iqm/station_control/interface/models/circuit.py +348 -0
  18. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/dynamic_quantum_architecture.py +61 -3
  19. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/jobs.py +41 -12
  20. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/observation_set.py +28 -4
  21. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/run.py +8 -8
  22. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/sweep.py +7 -1
  23. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/type_aliases.py +1 -2
  24. iqm_station_control_client-12.0.0/src/iqm/station_control/interface/py.typed +0 -0
  25. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/station_control.py +1 -1
  26. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0/src/iqm_station_control_client.egg-info}/PKG-INFO +3 -3
  27. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm_station_control_client.egg-info/SOURCES.txt +2 -19
  28. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm_station_control_client.egg-info/requires.txt +1 -1
  29. iqm_station_control_client-12.0.0/version.txt +1 -0
  30. iqm_station_control_client-11.3.1/MANIFEST.in +0 -10
  31. iqm_station_control_client-11.3.1/docs/changelog.rst +0 -2
  32. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/__init__.py +0 -14
  33. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/error.py +0 -30
  34. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/grpc_utils.py +0 -156
  35. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/iqm_server_client.py +0 -489
  36. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/__init__.py +0 -43
  37. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/calibration_pb2.py +0 -48
  38. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/calibration_pb2.pyi +0 -45
  39. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/calibration_pb2_grpc.py +0 -152
  40. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/common_pb2.py +0 -43
  41. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/common_pb2.pyi +0 -32
  42. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/common_pb2_grpc.py +0 -17
  43. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/job_pb2.py +0 -57
  44. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/job_pb2.pyi +0 -107
  45. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/job_pb2_grpc.py +0 -436
  46. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/qc_pb2.py +0 -51
  47. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/qc_pb2.pyi +0 -57
  48. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/qc_pb2_grpc.py +0 -163
  49. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/uuid_pb2.py +0 -39
  50. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/uuid_pb2.pyi +0 -26
  51. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/proto/uuid_pb2_grpc.py +0 -17
  52. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/testing/__init__.py +0 -13
  53. iqm_station_control_client-11.3.1/src/iqm/station_control/client/iqm_server/testing/iqm_server_mock.py +0 -102
  54. iqm_station_control_client-11.3.1/src/iqm/station_control/client/utils.py +0 -71
  55. iqm_station_control_client-11.3.1/version.txt +0 -1
  56. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/LICENSE.txt +0 -0
  57. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/README.rst +0 -0
  58. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/API.rst +0 -0
  59. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/Makefile +0 -0
  60. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/_static/css/custom.css +0 -0
  61. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/_static/images/favicon.ico +0 -0
  62. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/_static/images/logo.png +0 -0
  63. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/_templates/autosummary-class-template.rst +0 -0
  64. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/_templates/autosummary-module-template.rst +0 -0
  65. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/conf.py +0 -0
  66. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/index.rst +0 -0
  67. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/docs/license.rst +0 -0
  68. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/requirements/base.in +0 -0
  69. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/requirements/base.in.internal +0 -0
  70. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/setup.cfg +0 -0
  71. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/setup.py +0 -0
  72. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/__init__.py +0 -0
  73. /iqm_station_control_client-11.3.1/src/iqm/station_control/client/py.typed → /iqm_station_control_client-12.0.0/src/iqm/station_control/client/iqm_server/error.py +0 -0
  74. /iqm_station_control_client-11.3.1/src/iqm/station_control/interface/py.typed → /iqm_station_control_client-12.0.0/src/iqm/station_control/client/iqm_server/grpc_utils.py +0 -0
  75. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/__init__.py +0 -0
  76. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/channel_property_serializer.py +0 -0
  77. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/datetime_serializers.py +0 -0
  78. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/playlist_serializers.py +0 -0
  79. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/setting_node_serializer.py +0 -0
  80. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/sweep_serializers.py +0 -0
  81. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/client/serializers/task_serializers.py +0 -0
  82. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/__init__.py +0 -0
  83. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/list_with_meta.py +0 -0
  84. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/dut.py +0 -0
  85. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/monitor.py +0 -0
  86. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/observation.py +0 -0
  87. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/sequence.py +0 -0
  88. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/models/static_quantum_architecture.py +0 -0
  89. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm/station_control/interface/pydantic_base.py +0 -0
  90. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm_station_control_client.egg-info/dependency_links.txt +0 -0
  91. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/src/iqm_station_control_client.egg-info/top_level.txt +0 -0
  92. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/tests/.pylintrc +0 -0
  93. {iqm_station_control_client-11.3.1 → iqm_station_control_client-12.0.0}/tests/__init__.py +0 -0
@@ -1,11 +1,37 @@
1
- =========
2
- Changelog
3
- =========
4
-
5
- Version 11.3.1 (2025-10-27)
1
+ Version 12.0.0 (2025-11-19)
6
2
  ===========================
7
3
 
8
- - Bump version for 4.3.1 release. No functional changes.
4
+ Breaking changes
5
+ ----------------
6
+
7
+ - :class:`.IqmServerClient` has been removed from station-control-client package.
8
+ Use new REST-based implementation of :class:`.IQMServerClient` in iqm-client package.
9
+
10
+ Features
11
+ --------
12
+
13
+ - Add new circuit and job related models to station control interface,
14
+ used in IQM client when serializing/deserializing data in IQMServer communication
15
+ - Deprecate ``observation_ids`` in ``ObservationSetWithObservations``, use ``observations`` instead.
16
+ This is to unify the data with IQMServer.
17
+ - :class:`StationControlClient` now accepts the ``token`` and ``tokens_file`` parameters,
18
+ and can use the :envvar:`IQM_TOKEN` and :envvar:`IQM_TOKENS_FILE` environment variables
19
+ to get the authentication token.
20
+ - In case major versions differ, log warning instead of raising an error in the python package
21
+ version compatibility check when connecting to station-control server with
22
+ :class:`iqm.station_control.client.station_control.StationControlClient`.
23
+ - :class:`StationControlClient` now accepts the ``token`` and ``tokens_file`` parameters,
24
+ and can use the :envvar:`IQM_TOKEN` and :envvar:`IQM_TOKENS_FILE` environment variables
25
+ to get the authentication token.
26
+ - In case major versions differ, log warning instead of raising an error in the python package
27
+ version compatibility check when connecting to station-control server with
28
+ :class:`iqm.station_control.client.station_control.StationControlClient`.
29
+
30
+ Bug fixes
31
+ ---------
32
+
33
+ - Restrict RunDefinition fields to fix issues with serializing None and roundtrip resulting in empty dictionary.
34
+ - Fix python package compatibility version check when connecting to station-control server.
9
35
 
10
36
  Version 11.3.0 (2025-10-23)
11
37
  ===========================
@@ -382,7 +408,7 @@ Version 3.12.0 (2025-04-07)
382
408
  Features
383
409
  --------
384
410
 
385
- - Fix package version in published docs footers, :issue:`SW-1392`.
411
+ - Fix package version in published docs footers, :issue:`SW-1392`.
386
412
 
387
413
  Version 3.11.0 (2025-04-03)
388
414
  ===========================
@@ -827,7 +853,7 @@ Version 1.2 (2024-07-05)
827
853
 
828
854
  Features
829
855
  --------
830
- - Bump exa-common to 25.3
856
+ - Bump exa-common to 25.3
831
857
 
832
858
 
833
859
  Version 1.1 (2024-07-04)
@@ -0,0 +1,4 @@
1
+ include version.txt
2
+ include LICENSE.txt
3
+ prune tests/*
4
+ exclude .gitlab-ci.yml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-station-control-client
3
- Version: 11.3.1
3
+ Version: 12.0.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
@@ -204,7 +204,7 @@ License: Apache License
204
204
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
205
205
  See the License for the specific language governing permissions and
206
206
  limitations under the License.
207
- Project-URL: Documentation, https://iqm-finland.github.io/docs/iqm-station-control-client/
207
+ Project-URL: Documentation, https://docs.meetiqm.com/iqm-station-control-client/
208
208
  Project-URL: Homepage, https://pypi.org/project/iqm-station-control-client/
209
209
  Classifier: Development Status :: 4 - Beta
210
210
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -213,7 +213,6 @@ Classifier: Intended Audience :: Science/Research
213
213
  Requires-Python: >=3.11
214
214
  Description-Content-Type: text/x-rst
215
215
  License-File: LICENSE.txt
216
- Requires-Dist: iqm-exa-common<28,>=27
217
216
  Requires-Dist: iqm-data-definitions<3.0,>=2.18
218
217
  Requires-Dist: opentelemetry-exporter-otlp<2.0,>=1.25.0
219
218
  Requires-Dist: protobuf<5.0,>=4.25.3
@@ -225,6 +224,7 @@ Requires-Dist: requests==2.32.3
225
224
  Requires-Dist: types-requests
226
225
  Requires-Dist: tqdm>=4.59.0
227
226
  Requires-Dist: types-tqdm
227
+ Requires-Dist: iqm-exa-common
228
228
 
229
229
  Station control client library
230
230
  ==============================
@@ -0,0 +1,8 @@
1
+ .. _changelog:
2
+
3
+ =========
4
+ CHANGELOG
5
+ =========
6
+
7
+ .. include:: ../CHANGELOG.rst
8
+ :literal:
@@ -1,4 +1,3 @@
1
- # This file is managed by monotool.py, do not edit by hand
2
1
  [build-system]
3
2
  build-backend = "setuptools.build_meta"
4
3
  requires = [ "setuptools", "setuptools_scm[toml]",]
@@ -18,7 +17,7 @@ email = "info@meetiqm.com"
18
17
  file = "LICENSE.txt"
19
18
 
20
19
  [project.urls]
21
- Documentation = "https://iqm-finland.github.io/docs/iqm-station-control-client/"
20
+ Documentation = "https://docs.meetiqm.com/iqm-station-control-client/"
22
21
  Homepage = "https://pypi.org/project/iqm-station-control-client/"
23
22
 
24
23
  [tool.mypy]
@@ -58,7 +57,7 @@ nb_diff_ignore = [ "/metadata/language_info", "/metadata/widgets", "/cells/*/exe
58
57
 
59
58
  [tool.ruff.lint]
60
59
  ignore = [ "D203", "D213",]
61
- select = [ "E4", "E7", "E9", "E5", "F", "Q", "PL", "I", "D", "UP007", "UP006", "UP035", "ANN001", "ANN201", "ANN202",]
60
+ select = [ "E4", "E7", "E9", "E5", "F", "Q", "PL", "I", "D", "UP007", "UP006", "UP035", "ANN001", "ANN201", "ANN202", "NPY201",]
62
61
  unfixable = [ "F401",]
63
62
 
64
63
  [tool.ruff.lint.isort]
@@ -69,10 +68,10 @@ known-third-party = [ "exa", "iqm",]
69
68
  relative-imports-order = "closest-to-furthest"
70
69
 
71
70
  [tool.ruff.lint.per-file-ignores]
72
- "**/__init__.py" = [ "F401", "PLR0402",]
71
+ "**/__init__.py" = [ "D104", "F401", "PLR0402",]
73
72
  "**/docs/*" = [ "E402", "D100",]
74
73
  "**/setup.py" = [ "D100", "D103", "I001", "ANN201",]
75
- "**/src/*" = [ "PLR2004", "D400", "D415", "D205", "D401", "D417", "D100", "D101", "D107", "D102", "D105", "D103", "D404", "D104",]
74
+ "**/src/*" = [ "PLR2004", "D400", "D415", "D205", "D401", "D417", "D100", "D107", "D105", "D102", "D404",]
76
75
  "**/tests/*" = [ "F632", "PLR2004", "PLR0402", "PLC0414", "D", "ANN001", "ANN201", "ANN202",]
77
76
 
78
77
  [tool.ruff.lint.pylint]
@@ -1,4 +1,3 @@
1
- iqm-exa-common>=27,<28
2
1
  iqm-data-definitions >= 2.18, < 3.0
3
2
  opentelemetry-exporter-otlp >= 1.25.0, < 2.0
4
3
  protobuf >= 4.25.3, < 5.0
@@ -9,4 +8,5 @@ PyYAML >= 6.0, < 7.0
9
8
  requests == 2.32.3
10
9
  types-requests
11
10
  tqdm >= 4.59.0
12
- types-tqdm
11
+ types-tqdm
12
+ iqm-exa-common
@@ -0,0 +1,239 @@
1
+ # Copyright 2024 IQM client developers
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """This module contains user authentication related classes and functions required by IQMClient."""
15
+
16
+ from abc import ABC, abstractmethod
17
+ from base64 import b64decode
18
+ from collections.abc import Callable
19
+ import json
20
+ import os
21
+ import time
22
+ from typing import Any, TypeAlias
23
+
24
+ from iqm.iqm_server_client.errors import ClientAuthenticationError, ClientConfigurationError
25
+
26
+ REFRESH_MARGIN_SECONDS = 60
27
+
28
+
29
+ AuthHeaderCallback: TypeAlias = Callable[[], str]
30
+ """Function that returns an authorization header containing a bearer token."""
31
+
32
+
33
+ class TokenManager:
34
+ """TokenManager manages the access token required for user authentication.
35
+
36
+ Args:
37
+ token: Long-lived IQM token in plain text format.
38
+ tokens_file: Path to a tokens file used for authentication.
39
+ auth_header_callback: Callback function that returns an Authorization header containing a bearer token.
40
+ use_env_vars: Iff True, the first two parameters can also be read from the environment variables
41
+ :envvar:`IQM_TOKEN` and :envvar:`IQM_TOKENS_FILE`, respectively.
42
+ Environment variables can not be mixed with initialisation arguments.
43
+
44
+ At most one auth parameter should be given.
45
+
46
+ """
47
+
48
+ @staticmethod
49
+ def time_left_seconds(token: Any) -> int:
50
+ """Check how much time is left until the token expires.
51
+
52
+ Returns:
53
+ Time left on token in seconds.
54
+
55
+ """
56
+ if not token or not isinstance(token, str):
57
+ return 0
58
+ parts = token.split(".", 2)
59
+ if len(parts) != 3:
60
+ return 0
61
+ # Add padding to adjust body length to a multiple of 4 chars as required by base64 decoding
62
+ try:
63
+ body = parts[1] + ("=" * (-len(parts[1]) % 4))
64
+ exp_time = int(json.loads(b64decode(body)).get("exp", "0"))
65
+ return max(0, exp_time - int(time.time()))
66
+ except (UnicodeDecodeError, json.decoder.JSONDecodeError, ValueError, TypeError):
67
+ return 0
68
+
69
+ def __init__(
70
+ self,
71
+ token: str | None = None,
72
+ tokens_file: str | None = None,
73
+ auth_header_callback: AuthHeaderCallback | None = None,
74
+ *,
75
+ use_env_vars: bool = True,
76
+ ):
77
+ def _format_names(variable_names: list[str]) -> str:
78
+ """Format a list of variable names"""
79
+ return ", ".join(f'"{name}"' for name in variable_names)
80
+
81
+ auth_parameters: dict[str, str] = {}
82
+
83
+ init_parameters = {"token": token, "tokens_file": tokens_file}
84
+ init_params_given = [key for key, value in init_parameters.items() if value]
85
+ if auth_header_callback:
86
+ init_params_given.append("auth_header_callback")
87
+
88
+ if use_env_vars:
89
+ env_variables = {"token": "IQM_TOKEN", "tokens_file": "IQM_TOKENS_FILE"}
90
+ env_vars_given = [name for name in env_variables.values() if os.environ.get(name)]
91
+ else:
92
+ env_variables = {}
93
+ env_vars_given = []
94
+
95
+ if init_params_given and env_vars_given:
96
+ raise ClientConfigurationError(
97
+ "Authentication parameters given both as initialisation args and as environment variables: "
98
+ + f"initialisation args {_format_names(init_params_given)}, "
99
+ + f"environment variables {_format_names(env_vars_given)}."
100
+ + " Parameter sources must not be mixed."
101
+ )
102
+ auth_params_given = init_params_given + env_vars_given
103
+ if len(auth_params_given) > 1:
104
+ raise ClientConfigurationError(
105
+ f"No more than one authentication parameter may be given, received {auth_params_given}"
106
+ )
107
+
108
+ self._token_provider: TokenProviderInterface | None = None
109
+ self._auth_header_callback: AuthHeaderCallback | None = None
110
+ self._access_token: str | None = None
111
+
112
+ if auth_header_callback:
113
+ self._auth_header_callback = auth_header_callback
114
+ return
115
+
116
+ if env_vars_given:
117
+ auth_parameters = {key: value for key, name in env_variables.items() if (value := os.environ.get(name))}
118
+ else:
119
+ auth_parameters = {key: value for key, value in init_parameters.items() if value}
120
+
121
+ if not auth_parameters:
122
+ return # no authentication
123
+ if set(auth_parameters) == {"token"}:
124
+ # This is not necessarily a JWT token
125
+ self._token_provider = ExternalToken(auth_parameters["token"])
126
+ elif set(auth_parameters) == {"tokens_file"}:
127
+ self._token_provider = TokensFileReader(auth_parameters["tokens_file"])
128
+ else:
129
+ raise ClientConfigurationError("Not possible.")
130
+
131
+ def get_auth_header_callback(self) -> AuthHeaderCallback | None:
132
+ """Return a callback providing an Authorization header, or None if we cannot provide it."""
133
+ return self._get_bearer_token if self._token_provider else self._auth_header_callback
134
+
135
+ def _get_bearer_token(self, retries: int = 1) -> str:
136
+ """Return a valid bearer token.
137
+
138
+ Raises:
139
+ ClientAuthenticationError: getting the token failed
140
+ RuntimeError: There is no token provider (authentication disabled)
141
+
142
+ """
143
+ if self._token_provider is None:
144
+ raise RuntimeError("There is no token provider.")
145
+
146
+ # Use the existing access token if it is still valid
147
+ if TokenManager.time_left_seconds(self._access_token) > REFRESH_MARGIN_SECONDS:
148
+ return f"Bearer {self._access_token}"
149
+
150
+ # Otherwise, get a new access token from token provider
151
+ try:
152
+ self._access_token = self._token_provider.get_token()
153
+ return f"Bearer {self._access_token}"
154
+ except ClientAuthenticationError:
155
+ if retries < 1:
156
+ raise
157
+
158
+ # Try again
159
+ return self._get_bearer_token(retries - 1)
160
+
161
+ def close(self) -> bool:
162
+ """Close the configured token provider.
163
+
164
+ Returns:
165
+ True if closing was successful
166
+
167
+ Raises:
168
+ ClientAuthenticationError: closing failed
169
+
170
+ """
171
+ if self._token_provider is None:
172
+ return False
173
+
174
+ self._token_provider.close()
175
+ self._token_provider = None
176
+ return True
177
+
178
+
179
+ class TokenProviderInterface(ABC):
180
+ """Interface to token provider"""
181
+
182
+ @abstractmethod
183
+ def get_token(self) -> str:
184
+ """Return a valid access token.
185
+
186
+ Raises:
187
+ ClientAuthenticationError: acquiring the token failed
188
+
189
+ """
190
+
191
+ @abstractmethod
192
+ def close(self) -> None:
193
+ """Close the authentication session.
194
+
195
+ Raises:
196
+ ClientAuthenticationError: closing the session failed
197
+
198
+ """
199
+
200
+
201
+ class ExternalToken(TokenProviderInterface):
202
+ """Holds an external token"""
203
+
204
+ def __init__(self, token: str):
205
+ self._token: str | None = token
206
+
207
+ def get_token(self) -> str:
208
+ if self._token is None:
209
+ raise ClientAuthenticationError("No external token available")
210
+ return self._token
211
+
212
+ def close(self) -> None:
213
+ self._token = None
214
+ raise ClientAuthenticationError("Can not close externally managed auth session")
215
+
216
+
217
+ class TokensFileReader(TokenProviderInterface):
218
+ """Reads token from a file"""
219
+
220
+ def __init__(self, tokens_file: str):
221
+ self._path: str | None = tokens_file
222
+
223
+ def get_token(self) -> str:
224
+ try:
225
+ if self._path is None:
226
+ raise ClientAuthenticationError("No tokens file available")
227
+ with open(self._path, encoding="utf-8") as file:
228
+ raw_data = file.read()
229
+ json_data = json.loads(raw_data)
230
+ token = json_data.get("access_token")
231
+ if TokenManager.time_left_seconds(token) <= 0:
232
+ raise ClientAuthenticationError("Access token in file has expired or is not valid")
233
+ except (FileNotFoundError, IsADirectoryError, json.decoder.JSONDecodeError) as e:
234
+ raise ClientAuthenticationError(rf"Failed to read access token from file '{self._path}': {e}") from e
235
+ return token
236
+
237
+ def close(self) -> None:
238
+ self._path = None
239
+ raise ClientAuthenticationError("Can not close externally managed auth session")
@@ -32,6 +32,7 @@ from iqm.station_control.interface.models import (
32
32
  RunLite,
33
33
  SequenceMetadataData,
34
34
  StaticQuantumArchitecture,
35
+ TimelineEntry,
35
36
  )
36
37
  from iqm.station_control.interface.pydantic_base import PydanticBase
37
38
 
@@ -70,41 +71,45 @@ class ListModel(RootModel):
70
71
  )
71
72
 
72
73
 
73
- class DutList(ListModel):
74
+ class DutList(ListModel): # noqa: D101
74
75
  root: list[DutData]
75
76
 
76
77
 
77
- class DutFieldDataList(ListModel):
78
+ class DutFieldDataList(ListModel): # noqa: D101
78
79
  root: list[DutFieldData]
79
80
 
80
81
 
81
- class ObservationDataList(ListModel):
82
+ class ObservationDataList(ListModel): # noqa: D101
82
83
  root: list[ObservationData]
83
84
 
84
85
 
85
- class ObservationDefinitionList(ListModel):
86
+ class ObservationDefinitionList(ListModel): # noqa: D101
86
87
  root: list[ObservationDefinition]
87
88
 
88
89
 
89
- class ObservationLiteList(ListModel):
90
+ class ObservationLiteList(ListModel): # noqa: D101
90
91
  root: list[ObservationLite]
91
92
 
92
93
 
93
- class ObservationUpdateList(ListModel):
94
+ class ObservationUpdateList(ListModel): # noqa: D101
94
95
  root: list[ObservationUpdate]
95
96
 
96
97
 
97
- class ObservationSetDataList(ListModel):
98
+ class ObservationSetDataList(ListModel): # noqa: D101
98
99
  root: list[ObservationSetData]
99
100
 
100
101
 
101
- class SequenceMetadataDataList(ListModel):
102
+ class RunLiteList(ListModel): # noqa: D101
103
+ root: list[RunLite]
104
+
105
+
106
+ class SequenceMetadataDataList(ListModel): # noqa: D101
102
107
  root: list[SequenceMetadataData]
103
108
 
104
109
 
105
- class StaticQuantumArchitectureList(ListModel):
110
+ class StaticQuantumArchitectureList(ListModel): # noqa: D101
106
111
  root: list[StaticQuantumArchitecture]
107
112
 
108
113
 
109
- class RunLiteList(ListModel):
110
- root: list[RunLite]
114
+ class TimelineEntryList(ListModel): # noqa: D101
115
+ root: list[TimelineEntry]
@@ -23,7 +23,7 @@ from typing import Annotated, Any, Final, TypeAlias
23
23
  from pydantic import Field
24
24
  from pydantic.dataclasses import dataclass
25
25
 
26
- from iqm.station_control.interface.models.observation import ObservationBase
26
+ from iqm.station_control.interface.models import ObservationBase
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
@@ -40,8 +40,8 @@ def serialize_run_definition(run_definition: RunDefinition) -> RunDefinitionProt
40
40
  username=run_definition.username,
41
41
  experiment_name=run_definition.experiment_name,
42
42
  experiment_label=run_definition.experiment_label,
43
- options=serialize_struct(run_definition.options), # type: ignore[arg-type]
44
- additional_run_properties=serialize_struct(run_definition.additional_run_properties), # type: ignore[arg-type]
43
+ options=serialize_struct(run_definition.options),
44
+ additional_run_properties=serialize_struct(run_definition.additional_run_properties),
45
45
  software_version_set_id=run_definition.software_version_set_id,
46
46
  components=run_definition.components,
47
47
  default_data_parameters=run_definition.default_data_parameters,
@@ -50,8 +50,9 @@ def serialize_run_definition(run_definition: RunDefinition) -> RunDefinitionProt
50
50
  run_definition_proto.sweep_definition_payload.Pack(
51
51
  serialize_sweep_definition(run_definition.sweep_definition), type_url_prefix="iqm-data-definitions"
52
52
  )
53
- for key, sweep in run_definition.hard_sweeps.items(): # type: ignore[union-attr]
53
+ for key, sweep in run_definition.hard_sweeps.items():
54
54
  run_definition_proto.hard_sweeps[key].CopyFrom(proto_serialization.nd_sweep.pack(sweep, minimal=False))
55
+
55
56
  return run_definition_proto
56
57
 
57
58
 
@@ -94,7 +95,7 @@ def serialize_run_data(run_data: RunData) -> dict:
94
95
  "options": run_data.options,
95
96
  "additional_run_properties": run_data.additional_run_properties,
96
97
  "software_version_set_id": run_data.software_version_set_id,
97
- "hard_sweeps": {key: encode_nd_sweeps(value) for key, value in run_data.hard_sweeps.items()}, # type: ignore[union-attr]
98
+ "hard_sweeps": {key: encode_nd_sweeps(value) for key, value in run_data.hard_sweeps.items()},
98
99
  "components": run_data.components,
99
100
  "default_data_parameters": run_data.default_data_parameters,
100
101
  "default_sweep_parameters": run_data.default_sweep_parameters,
@@ -33,7 +33,7 @@ def deserialize_struct(proto: pb.Struct) -> dict:
33
33
  return {key: _deserialize_value(value) for key, value in proto.fields.items()}
34
34
 
35
35
 
36
- def _serialize_value(value: None | float | str | bool | dict | list | int) -> pb.Value:
36
+ def _serialize_value(value: None | float | str | bool | dict | list) -> pb.Value:
37
37
  """Serialize a value into a Value protobuf representation."""
38
38
  if value is None:
39
39
  return pb.Value(null_value=True)