ni.measurementlink.sessionmanagement.v1.client 0.1.0.dev0__tar.gz → 0.1.0.dev1__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.

Potentially problematic release.


This version of ni.measurementlink.sessionmanagement.v1.client might be problematic. Click here for more details.

Files changed (22) hide show
  1. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/PKG-INFO +3 -3
  2. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/pyproject.toml +21 -9
  3. ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1/src/ni/measurementlink/sessionmanagement/v1/client/_annotations.py +79 -0
  4. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_client.py +44 -52
  5. ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1/src/ni/measurementlink/sessionmanagement/v1/client/_client_base.py +100 -0
  6. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_reservation.py +8 -8
  7. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_types.py +18 -0
  8. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/README.md +0 -0
  9. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/__init__.py +0 -0
  10. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_constants.py +0 -0
  11. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/__init__.py +0 -0
  12. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_configuration.py +0 -0
  13. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_dotenvpath.py +0 -0
  14. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_grpcdevice.py +0 -0
  15. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_nidaqmx.py +0 -0
  16. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_nidcpower.py +0 -0
  17. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_nidigital.py +0 -0
  18. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_nidmm.py +0 -0
  19. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_nifgen.py +0 -0
  20. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_niscope.py +0 -0
  21. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/_drivers/_niswitch.py +0 -0
  22. {ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev0 → ni_measurementlink_sessionmanagement_v1_client-0.1.0.dev1}/src/ni/measurementlink/sessionmanagement/v1/client/py.typed +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ni.measurementlink.sessionmanagement.v1.client
3
- Version: 0.1.0.dev0
3
+ Version: 0.1.0.dev1
4
4
  Summary: Client gRPC APIs for the session management service
5
5
  License: MIT
6
6
  Keywords: ni-apis,sessionmanagement
7
7
  Author: NI
8
8
  Author-email: opensource@ni.com
9
- Maintainer: Brad Keryan
10
- Maintainer-email: brad.keryan@emerson.com
9
+ Maintainer: Joe Friedrichsen
10
+ Maintainer-email: joe.friedrichsen@emerson.com
11
11
  Requires-Python: >=3.9,<4.0
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
@@ -1,15 +1,14 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "ni.measurementlink.sessionmanagement.v1.client"
3
- version = "0.1.0-dev0"
3
+ version = "0.1.0-dev1"
4
4
  license = "MIT"
5
5
  description = "Client gRPC APIs for the session management service"
6
- authors = ["NI <opensource@ni.com>"]
6
+ authors = [{name = "NI", email = "opensource@ni.com"}]
7
7
  maintainers = [
8
- "Brad Keryan <brad.keryan@emerson.com>",
9
- "Joe Friedrichsen <joe.friedrichsen@emerson.com>",
8
+ {name = "Joe Friedrichsen", email = "joe.friedrichsen@emerson.com"},
9
+ {name = "Brad Keryan", email = "brad.keryan@emerson.com"}
10
10
  ]
11
11
  readme = "README.md"
12
- repository = "https://github.com/ni/ni-apis-python"
13
12
  keywords = ["ni-apis", "sessionmanagement"]
14
13
  classifiers = [
15
14
  "Development Status :: 3 - Alpha",
@@ -27,10 +26,17 @@ classifiers = [
27
26
  "Programming Language :: Python :: 3.13",
28
27
  "Programming Language :: Python :: Implementation :: CPython",
29
28
  ]
29
+ dynamic = ["dependencies"]
30
+ requires-python = '>=3.9,<4.0'
31
+
32
+ [project.urls]
33
+ repository = "https://github.com/ni/ni-apis-python"
34
+
35
+ [tool.poetry]
30
36
  packages = [{include = "ni", from = "src"}]
37
+ requires-poetry = '>=2.1,<3.0'
31
38
 
32
39
  [tool.poetry.dependencies]
33
- python = "^3.9"
34
40
  protobuf = {version=">=4.21"}
35
41
  pywin32 = { version = ">=303", platform = "win32" }
36
42
  deprecation = ">=2.1"
@@ -53,6 +59,7 @@ ni-measurementlink-discovery-v1-client = { version = ">=0.1.0.dev0", allow-prere
53
59
  [tool.poetry.group.dev.dependencies]
54
60
  types-grpcio = ">=1.0"
55
61
  types-protobuf = ">=4.21"
62
+ types-pywin32 = ">=311.0.0.20250822"
56
63
 
57
64
  [tool.poetry.group.lint.dependencies]
58
65
  bandit = { version = ">=1.7", extras = ["toml"] }
@@ -91,14 +98,18 @@ optional = true
91
98
 
92
99
  [tool.poetry.group.docs.dependencies]
93
100
  # The latest Sphinx requires a recent Python version.
94
- Sphinx = { version = ">=8.2", python = "^3.11" }
101
+ Sphinx = [
102
+ { version = ">=7.4", python = ">=3.9,<3.10" },
103
+ { version = ">=8.1", python = ">=3.10,<3.11" },
104
+ { version = ">=8.2", python = "^3.11" },
105
+ ]
95
106
  sphinx-rtd-theme = ">=1.0.0"
96
107
  sphinx-autoapi = ">=1.8.4"
97
108
  m2r2 = ">=0.3.2"
98
109
  toml = ">=0.10.2"
99
110
 
100
111
  [build-system]
101
- requires = ["poetry-core>=1.8"]
112
+ requires = ["poetry-core>=2.1.0,<3.0"]
102
113
  build-backend = "poetry.core.masonry.api"
103
114
 
104
115
 
@@ -109,6 +120,7 @@ skips = [
109
120
 
110
121
  [tool.ni-python-styleguide]
111
122
  extend_exclude = "docs,*_pb2_grpc.py,*_pb2_grpc.pyi,*_pb2.py,*_pb2.pyi"
123
+ application-import-names = "ni.measurementlink.sessionmanagement.v1.client"
112
124
 
113
125
  [tool.black]
114
126
  extend-exclude = 'docs/|_pb2(_grpc)?\.(py|pyi)$'
@@ -0,0 +1,79 @@
1
+ import os
2
+ import socket
3
+ import sys
4
+
5
+ from ni.measurementlink.sessionmanagement.v1.annotations import (
6
+ REGISTERED_HOSTNAME,
7
+ REGISTERED_IPADDRESS,
8
+ REGISTERED_USERNAME,
9
+ RESERVED_HOSTNAME,
10
+ RESERVED_IPADDRESS,
11
+ RESERVED_USERNAME,
12
+ )
13
+
14
+
15
+ def get_machine_details() -> tuple[dict[str, str], dict[str, str]]:
16
+ """Get the machine details for reserved and registered annotations."""
17
+ hostname = _get_hostname()
18
+ username = _get_username()
19
+ ip_address = _get_ip_address()
20
+
21
+ reserved = {
22
+ RESERVED_HOSTNAME: hostname,
23
+ RESERVED_USERNAME: username,
24
+ RESERVED_IPADDRESS: ip_address,
25
+ }
26
+
27
+ registered = {
28
+ REGISTERED_HOSTNAME: hostname,
29
+ REGISTERED_USERNAME: username,
30
+ REGISTERED_IPADDRESS: ip_address,
31
+ }
32
+
33
+ return reserved, registered
34
+
35
+
36
+ def remove_reservation_annotations(annotations: dict[str, str] | None) -> dict[str, str]:
37
+ """Remove reserved annotations from the provided annotations."""
38
+ if annotations is None:
39
+ return {}
40
+ reservation_keys = {
41
+ RESERVED_HOSTNAME,
42
+ RESERVED_USERNAME,
43
+ RESERVED_IPADDRESS,
44
+ }
45
+ return {k: v for k, v in annotations.items() if k not in reservation_keys}
46
+
47
+
48
+ def _get_hostname() -> str:
49
+ if sys.platform == "win32":
50
+ try:
51
+ import win32api # pyright: ignore[reportMissingModuleSource]
52
+
53
+ return win32api.GetComputerName()
54
+ except Exception:
55
+ return ""
56
+ else:
57
+ return socket.gethostname()
58
+
59
+
60
+ def _get_username() -> str:
61
+ if sys.platform == "win32":
62
+ try:
63
+ import win32api # pyright: ignore[reportMissingModuleSource]
64
+
65
+ return win32api.GetUserName()
66
+ except Exception:
67
+ return ""
68
+ else:
69
+ return os.environ.get("USER", "")
70
+
71
+
72
+ def _get_ip_address() -> str:
73
+ try:
74
+ ipv4_addresses = [
75
+ info[4][0] for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET)
76
+ ]
77
+ return str(ipv4_addresses[0]) if ipv4_addresses else ""
78
+ except Exception:
79
+ return ""
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- import threading
7
6
  import warnings
8
7
  from collections.abc import Iterable, Mapping
9
8
 
@@ -14,6 +13,13 @@ import ni.measurementlink.sessionmanagement.v1.session_management_service_pb2_gr
14
13
  from ni.measurementlink.discovery.v1.client import DiscoveryClient
15
14
  from ni_grpc_extensions.channelpool import GrpcChannelPool
16
15
 
16
+ from ni.measurementlink.sessionmanagement.v1.client._annotations import (
17
+ get_machine_details,
18
+ remove_reservation_annotations,
19
+ )
20
+ from ni.measurementlink.sessionmanagement.v1.client._client_base import (
21
+ GrpcServiceClientBase,
22
+ )
17
23
  from ni.measurementlink.sessionmanagement.v1.client._constants import (
18
24
  GRPC_SERVICE_CLASS,
19
25
  GRPC_SERVICE_INTERFACE_NAME,
@@ -32,8 +38,12 @@ from ni.measurementlink.sessionmanagement.v1.client._types import (
32
38
  _logger = logging.getLogger(__name__)
33
39
 
34
40
 
35
- class SessionManagementClient:
36
- """Client for accessing the measurement plug-in session management service."""
41
+ class SessionManagementClient(
42
+ GrpcServiceClientBase[session_management_service_pb2_grpc.SessionManagementServiceStub]
43
+ ):
44
+ """Client for accessing the NI Session Management Service via gRPC."""
45
+
46
+ __slots__ = ("_reserved_annotations", "_registered_annotations")
37
47
 
38
48
  def __init__(
39
49
  self,
@@ -42,54 +52,16 @@ class SessionManagementClient:
42
52
  grpc_channel: grpc.Channel | None = None,
43
53
  grpc_channel_pool: GrpcChannelPool | None = None,
44
54
  ) -> None:
45
- """Initialize session management client.
46
-
47
- Args:
48
- discovery_client: An optional discovery client (recommended).
49
-
50
- grpc_channel: An optional session management gRPC channel.
51
-
52
- grpc_channel_pool: An optional gRPC channel pool (recommended).
53
- """
54
- self._initialization_lock = threading.Lock()
55
- self._discovery_client = discovery_client
56
- self._grpc_channel_pool = grpc_channel_pool
57
- self._stub: session_management_service_pb2_grpc.SessionManagementServiceStub | None = None
58
-
59
- if grpc_channel is not None:
60
- self._stub = session_management_service_pb2_grpc.SessionManagementServiceStub(
61
- grpc_channel
62
- )
63
-
64
- def _get_stub(self) -> session_management_service_pb2_grpc.SessionManagementServiceStub:
65
- if self._stub is None:
66
- with self._initialization_lock:
67
- if self._grpc_channel_pool is None:
68
- _logger.debug("Creating unshared GrpcChannelPool.")
69
- self._grpc_channel_pool = GrpcChannelPool()
70
- if self._discovery_client is None:
71
- _logger.debug("Creating unshared DiscoveryClient.")
72
- self._discovery_client = DiscoveryClient(
73
- grpc_channel_pool=self._grpc_channel_pool
74
- )
75
- if self._stub is None:
76
- compute_nodes = self._discovery_client.enumerate_compute_nodes()
77
- remote_compute_nodes = [node for node in compute_nodes if not node.is_local]
78
- # Use remote node URL as deployment target if only one remote node is found.
79
- # If more than one remote node exists, use empty string for deployment target.
80
- first_remote_node_url = (
81
- remote_compute_nodes[0].url if len(remote_compute_nodes) == 1 else ""
82
- )
83
- service_location = self._discovery_client.resolve_service(
84
- provided_interface=GRPC_SERVICE_INTERFACE_NAME,
85
- deployment_target=first_remote_node_url,
86
- service_class=GRPC_SERVICE_CLASS,
87
- )
88
- channel = self._grpc_channel_pool.get_channel(service_location.insecure_address)
89
- self._stub = session_management_service_pb2_grpc.SessionManagementServiceStub(
90
- channel
91
- )
92
- return self._stub
55
+ """Initialize a SessionManagementClient instance."""
56
+ self._reserved_annotations, self._registered_annotations = get_machine_details()
57
+ super().__init__(
58
+ discovery_client=discovery_client,
59
+ grpc_channel=grpc_channel,
60
+ grpc_channel_pool=grpc_channel_pool,
61
+ service_interface_name=GRPC_SERVICE_INTERFACE_NAME,
62
+ service_class=GRPC_SERVICE_CLASS,
63
+ stub_class=session_management_service_pb2_grpc.SessionManagementServiceStub,
64
+ )
93
65
 
94
66
  def reserve_session(
95
67
  self,
@@ -214,6 +186,7 @@ class SessionManagementClient:
214
186
  request = session_management_service_pb2.ReserveSessionsRequest(
215
187
  pin_map_context=context._to_grpc(),
216
188
  timeout_in_milliseconds=_timeout_to_milliseconds(timeout),
189
+ annotations=self._reserved_annotations,
217
190
  )
218
191
  if instrument_type_id is not None:
219
192
  request.instrument_type_id = instrument_type_id
@@ -239,6 +212,15 @@ class SessionManagementClient:
239
212
  Args:
240
213
  session_info: Sessions to register.
241
214
  """
215
+ session_info = [
216
+ info._replace(
217
+ annotations={
218
+ **remove_reservation_annotations(info.annotations),
219
+ **self._registered_annotations,
220
+ }
221
+ )
222
+ for info in session_info
223
+ ]
242
224
  request = session_management_service_pb2.RegisterSessionsRequest(
243
225
  sessions=(info._to_grpc_v1() for info in session_info),
244
226
  )
@@ -287,7 +269,8 @@ class SessionManagementClient:
287
269
  unreserve them.
288
270
  """
289
271
  request = session_management_service_pb2.ReserveAllRegisteredSessionsRequest(
290
- timeout_in_milliseconds=_timeout_to_milliseconds(timeout)
272
+ timeout_in_milliseconds=_timeout_to_milliseconds(timeout),
273
+ annotations=self._reserved_annotations,
291
274
  )
292
275
  if instrument_type_id is not None:
293
276
  request.instrument_type_id = instrument_type_id
@@ -307,6 +290,15 @@ class SessionManagementClient:
307
290
  Args:
308
291
  multiplexer_session_info: Sessions to register.
309
292
  """
293
+ multiplexer_session_info = [
294
+ info._replace(
295
+ annotations={
296
+ **remove_reservation_annotations(info.annotations),
297
+ **self._registered_annotations,
298
+ }
299
+ )
300
+ for info in multiplexer_session_info
301
+ ]
310
302
  request = session_management_service_pb2.RegisterMultiplexerSessionsRequest(
311
303
  multiplexer_sessions=(info._to_grpc_v1() for info in multiplexer_session_info),
312
304
  )
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import threading
5
+ from typing import Generic, Protocol, TypeVar
6
+
7
+ import grpc
8
+ from ni.measurementlink.discovery.v1.client import DiscoveryClient
9
+ from ni_grpc_extensions.channelpool import GrpcChannelPool
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class StubProtocol(Protocol):
15
+ """Protocol for gRPC stub classes."""
16
+
17
+ def __init__(self, channel: grpc.Channel) -> None:
18
+ """Initialize the gRPC client."""
19
+
20
+
21
+ TStub = TypeVar("TStub", bound=StubProtocol)
22
+
23
+
24
+ class GrpcServiceClientBase(Generic[TStub]):
25
+ """Base class for NI gRPC service clients."""
26
+
27
+ __slots__ = (
28
+ "_initialization_lock",
29
+ "_discovery_client",
30
+ "_grpc_channel_pool",
31
+ "_stub",
32
+ "_service_interface_name",
33
+ "_service_class",
34
+ "_stub_class",
35
+ )
36
+
37
+ _initialization_lock: threading.Lock
38
+ _discovery_client: DiscoveryClient | None
39
+ _grpc_channel_pool: GrpcChannelPool | None
40
+ _stub: TStub | None
41
+ _service_interface_name: str
42
+ _service_class: str
43
+ _stub_class: type[TStub]
44
+
45
+ def __init__(
46
+ self,
47
+ service_interface_name: str,
48
+ service_class: str,
49
+ stub_class: type[TStub],
50
+ *,
51
+ discovery_client: DiscoveryClient | None = None,
52
+ grpc_channel: grpc.Channel | None = None,
53
+ grpc_channel_pool: GrpcChannelPool | None = None,
54
+ ) -> None:
55
+ """Initialize the gRPC client.
56
+
57
+ Args:
58
+ service_interface_name: The fully qualified name of the service interface.
59
+ service_class: The name of the service class.
60
+ stub_class: The gRPC stub class for the service.
61
+ discovery_client: An optional discovery client (recommended).
62
+ grpc_channel: An optional pin map gRPC channel.
63
+ grpc_channel_pool: An optional gRPC channel pool (recommended).
64
+ """
65
+ self._initialization_lock = threading.Lock()
66
+ self._discovery_client = discovery_client
67
+ self._grpc_channel_pool = grpc_channel_pool
68
+ self._stub = stub_class(grpc_channel) if grpc_channel is not None else None
69
+ self._service_interface_name = service_interface_name
70
+ self._service_class = service_class
71
+ self._stub_class = stub_class
72
+
73
+ def _get_stub(self) -> TStub:
74
+ if self._stub is None:
75
+ with self._initialization_lock:
76
+ if self._grpc_channel_pool is None:
77
+ _logger.debug("Creating unshared GrpcChannelPool.")
78
+ self._grpc_channel_pool = GrpcChannelPool()
79
+
80
+ if self._discovery_client is None:
81
+ _logger.debug("Creating unshared DiscoveryClient.")
82
+ self._discovery_client = DiscoveryClient(
83
+ grpc_channel_pool=self._grpc_channel_pool
84
+ )
85
+
86
+ if self._stub is None:
87
+ compute_nodes = self._discovery_client.enumerate_compute_nodes()
88
+ remote_nodes = [node for node in compute_nodes if not node.is_local]
89
+ target_url = remote_nodes[0].url if len(remote_nodes) == 1 else ""
90
+
91
+ service_location = self._discovery_client.resolve_service(
92
+ provided_interface=self._service_interface_name,
93
+ deployment_target=target_url,
94
+ service_class=self._service_class,
95
+ )
96
+
97
+ channel = self._grpc_channel_pool.get_channel(service_location.insecure_address)
98
+ self._stub = self._stub_class(channel)
99
+
100
+ return self._stub
@@ -172,15 +172,17 @@ class _BaseSessionContainer(abc.ABC):
172
172
 
173
173
  @property
174
174
  def _discovery_client(self) -> DiscoveryClient:
175
- if not self._session_management_client._discovery_client:
175
+ client = self._session_management_client._discovery_client
176
+ if client is None:
176
177
  raise ValueError("This method requires a discovery client.")
177
- return self._session_management_client._discovery_client
178
+ return client
178
179
 
179
180
  @property
180
181
  def _grpc_channel_pool(self) -> GrpcChannelPool:
181
- if not self._session_management_client._grpc_channel_pool:
182
+ pool = self._session_management_client._grpc_channel_pool
183
+ if pool is None:
182
184
  raise ValueError("This method requires a gRPC channel pool.")
183
- return self._session_management_client._grpc_channel_pool
185
+ return pool
184
186
 
185
187
 
186
188
  class MultiplexerSessionContainer(_BaseSessionContainer):
@@ -274,9 +276,7 @@ class MultiplexerSessionContainer(_BaseSessionContainer):
274
276
  return multiplexer_session_infos
275
277
 
276
278
  @contextlib.contextmanager
277
- def _cache_multiplexer_session(
278
- self, session_name: str, session: TMultiplexerSession
279
- ) -> Generator[None]: # pyright: ignore[reportInvalidTypeVarUse]
279
+ def _cache_multiplexer_session(self, session_name: str, session: object) -> Generator[None]:
280
280
  if session_name in self._multiplexer_session_cache:
281
281
  raise RuntimeError(f"Multiplexer session '{session_name}' already exists.")
282
282
  self._multiplexer_session_cache[session_name] = session
@@ -664,7 +664,7 @@ class BaseReservation(_BaseSessionContainer):
664
664
  self._session_management_client._unreserve_sessions(self._grpc_session_info)
665
665
 
666
666
  @contextlib.contextmanager
667
- def _cache_session(self, session_name: str, session: TSession) -> Generator[None]:
667
+ def _cache_session(self, session_name: str, session: object) -> Generator[None]:
668
668
  if session_name in self._session_cache:
669
669
  raise RuntimeError(f"Session '{session_name}' already exists.")
670
670
  self._session_cache[session_name] = session
@@ -139,6 +139,13 @@ class SessionInformation(NamedTuple):
139
139
  This field is None until the appropriate initialize_session(s) method is called.
140
140
  """
141
141
 
142
+ annotations: dict[str, str] | None = None
143
+ """Annotations to attach to the session.
144
+
145
+ This field is optional and can be used to store any additional metadata
146
+ related to the session.
147
+ """
148
+
142
149
  def _check_runtime_type(self, session_type: type) -> None:
143
150
  if not isinstance(self.session, session_type):
144
151
  raise TypeError(
@@ -162,6 +169,7 @@ class SessionInformation(NamedTuple):
162
169
  instrument_type_id=other.instrument_type_id,
163
170
  session_exists=other.session_exists,
164
171
  channel_mappings=[ChannelMapping._from_grpc_v1(m) for m in other.channel_mappings],
172
+ annotations=dict(other.annotations) if other.annotations else None,
165
173
  )
166
174
 
167
175
  def _to_grpc_v1(
@@ -174,6 +182,7 @@ class SessionInformation(NamedTuple):
174
182
  instrument_type_id=self.instrument_type_id,
175
183
  session_exists=self.session_exists,
176
184
  channel_mappings=[m._to_grpc_v1() for m in self.channel_mappings],
185
+ annotations=self.annotations if self.annotations else {},
177
186
  )
178
187
 
179
188
 
@@ -253,6 +262,13 @@ class MultiplexerSessionInformation(NamedTuple):
253
262
  This field is None until the appropriate initialize_multiplexer_session(s) method is called.
254
263
  """
255
264
 
265
+ annotations: dict[str, str] = {}
266
+ """Annotations to attach to the session.
267
+
268
+ This field is optional and can be used to store any additional metadata
269
+ related to the session.
270
+ """
271
+
256
272
  def _check_runtime_type(self, multiplexer_session_type: type) -> None:
257
273
  if not isinstance(self.session, multiplexer_session_type):
258
274
  raise TypeError(
@@ -274,6 +290,7 @@ class MultiplexerSessionInformation(NamedTuple):
274
290
  resource_name=other.resource_name,
275
291
  multiplexer_type_id=other.multiplexer_type_id,
276
292
  session_exists=other.session_exists,
293
+ annotations=dict(other.annotations),
277
294
  )
278
295
 
279
296
  def _to_grpc_v1(self) -> session_management_service_pb2.MultiplexerSessionInformation:
@@ -282,6 +299,7 @@ class MultiplexerSessionInformation(NamedTuple):
282
299
  resource_name=self.resource_name,
283
300
  multiplexer_type_id=self.multiplexer_type_id,
284
301
  session_exists=self.session_exists,
302
+ annotations=self.annotations,
285
303
  )
286
304
 
287
305