Skip to content

Link¤

Link(
    *,
    client: Client,
    curve: AuthCurve | None,
    host: str,
    plain: AuthPlain | None,
    service_name: str,
    xpub_port: int
)

A link with a Service.

Warning

Do not instantiate directly. Use the link method to create a Link instance.

Source code in src/msl/network/client.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def __init__(  # noqa: PLR0913
    self,
    *,
    client: Client,
    curve: AuthCurve | None,
    host: str,
    plain: AuthPlain | None,
    service_name: str,
    xpub_port: int,
) -> None:
    """A link with a [Service][].

    !!! warning
        Do not instantiate directly. Use the [link][Client.link] method
        to create a [Link][] instance.
    """
    self.flag_at: Callable[[Flag], AbstractContextManager[None]] = client.flag_at
    """Reference to [flag_at][Client.flag_at]."""

    self.service_name: str = service_name
    """[str][] — The name of the [Service][] that the link is with."""

    self.timeout: float | None = None
    """[float][] | `None` — The number of seconds to wait for a reply from a *synchronous* request.

    The value is initially `None` for new links, which means that there is no limit on the wait time.
    """

    self._request: Callable[..., Future[Any]] = client._request  # pyright: ignore[reportPrivateUsage]  # noqa: SLF001
    self._link_subscriber: _LinkSubscriber = _LinkSubscriber(
        service_name=service_name,
        host=host,
        curve=curve,
        plain=plain,
        xpub_port=xpub_port,
    )
    self._client: Client = client

    self._thread: Thread = Thread(
        target=run_event_loop, daemon=True, args=(_create_async_subscriber(self._link_subscriber),)
    )
    self._thread.start()

    while self._link_subscriber.sub_socket is None:
        continue

flag_at instance-attribute ¤

Reference to flag_at.

service_name instance-attribute ¤

service_name: str

str — The name of the Service that the link is with.

timeout instance-attribute ¤

timeout: float | None

float | None — The number of seconds to wait for a reply from a synchronous request.

The value is initially None for new links, which means that there is no limit on the wait time.

__getattr__ ¤

__getattr__(attr: str) -> FutureOrResult

All undefined attributes are sent to the linked Service to process.

The internal wrapper function that is returned is essentially:

def wrapper(*args: Any, sync: bool = True, **kwargs: Any) -> Any | Future[Any]:
    future = request(self.service_name, attr, *args, **kwargs)
    if sync:
        return future.result(self.timeout)
    return future
return wrapper

Warning

Your Service subclass cannot have method/attribute names that are the same as those found in the Link class, otherwise a request will not be sent to your Service because the Link instance will handle the method call.

Source code in src/msl/network/client.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def __getattr__(self, attr: str) -> FutureOrResult:
    """All undefined attributes are sent to the linked [Service][] to process.

    The internal wrapper function that is returned is essentially:

    ```python
    def wrapper(*args: Any, sync: bool = True, **kwargs: Any) -> Any | Future[Any]:
        future = request(self.service_name, attr, *args, **kwargs)
        if sync:
            return future.result(self.timeout)
        return future
    return wrapper
    ```

    !!! warning
        Your [Service][] subclass cannot have method/attribute names that are the same as those
        found in the [Link][] class, otherwise a request will not be sent to your [Service][]
        because the [Link][] instance will handle the method call.
    """

    def wrapper(*args: Any, sync: bool = True, **kwargs: Any) -> Any | Future[Any]:
        """Returns the result, if `sync=True`, otherwise a future that will eventually contain the result."""
        future = self._request(self.service_name, attr, *args, **kwargs)
        if sync:
            return future.result(self.timeout)
        return future

    return wrapper

signatures ¤

signatures(timeout: float | None = None) -> dict[str, str]

Get the function signatures that the linked Service provides.

For example,

{
    "get_resolution": "() -> tuple[int, int]",
    "set_resolution": "(width: int, height: int) -> None",
    "stream": "() -> None",
    "version": "() -> str"
}

Parameters:

Name Type Description Default
timeout float | None

The maximum number of seconds to wait for the reply. If None, then the value of timeout is used.

None

Returns:

Type Description
dict[str, str]

A mapping between the function (attribute) name and the function signature (attribute type).

Source code in src/msl/network/client.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def signatures(self, timeout: float | None = None) -> dict[str, str]:
    """Get the function signatures that the linked [Service][] provides.

    For example,

    ```json
    {
        "get_resolution": "() -> tuple[int, int]",
        "set_resolution": "(width: int, height: int) -> None",
        "stream": "() -> None",
        "version": "() -> str"
    }
    ```

    Args:
        timeout: The maximum number of seconds to wait for the reply.
            If `None`, then the value of [timeout][..timeout] is used.

    Returns:
        A mapping between the function (attribute) name and the
            function signature (attribute type).
    """
    t = self.timeout if timeout is None else timeout
    future = self._request(self.service_name, "signatures")
    signatures: dict[str, str] = future.result(t)
    return signatures

subscribe ¤

subscribe(callback: Callable[[Any], None]) -> None

Subscribe to publications from the linked Service.

Parameters:

Name Type Description Default
callback Callable[[Any], None]

A callback function to receive the published result. The callback receives a single argument, the published result, and the returned value is ignored.

required
Source code in src/msl/network/client.py
325
326
327
328
329
330
331
332
333
334
335
336
337
def subscribe(self, callback: Callable[[Any], None]) -> None:
    """Subscribe to publications from the linked [Service][].

    Args:
        callback: A callback function to receive the published *result*. The callback
            receives a single argument, the published *result*, and the returned value is ignored.
    """
    if self._link_subscriber.sub_socket is None:
        msg = f"Cannot subscribe to {self.service_name!r}, unlinked from the service"
        raise RuntimeError(msg)

    self._link_subscriber.sub_socket.setsockopt(zmq.SUBSCRIBE, self.service_name.encode())
    self._link_subscriber.callback = callback

timeout_at ¤

timeout_at(timeout: float | None) -> Generator[None, None, None]

Use as a context manager to temporarily change the timeout value.

Example

from msl.network import Client

with Client() as c:
    camera = c.link("Camera")
    camera.timeout = 5  # (1)!

    # uses a timeout of 5 seconds
    resolution = camera.get_resolution()

    with camera.timeout_at(None):  # (2)!
        # no timeout
        images = camera.capture(n=100)

    # uses a timeout of 5 seconds
    shutter_speed = camera.get_shutter_speed()
  1. Use a timeout value of 5 seconds for each synchronous request.
  2. Temporarily disable the timeout to capture 100 images.

Parameters:

Name Type Description Default
timeout float | None

The temporary timeout value to use for synchronous requests while within the context. Once the context exits, the value is reset to the original value.

required
Source code in src/msl/network/client.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
@contextmanager
def timeout_at(self, timeout: float | None) -> Generator[None, None, None]:
    """Use as a context manager to temporarily change the [timeout][..timeout] value.

    !!! example
        ```python hl_lines="10-12"
        from msl.network import Client

        with Client() as c:
            camera = c.link("Camera")
            camera.timeout = 5  # (1)!

            # uses a timeout of 5 seconds
            resolution = camera.get_resolution()

            with camera.timeout_at(None):  # (2)!
                # no timeout
                images = camera.capture(n=100)

            # uses a timeout of 5 seconds
            shutter_speed = camera.get_shutter_speed()
        ```

        1. Use a timeout value of 5 seconds for each synchronous request.
        2. Temporarily disable the timeout to capture 100 images.

    Args:
        timeout: The temporary timeout value to use for synchronous requests while within
            the context. Once the context exits, the value is reset to the original value.
    """
    original = self.timeout
    self.timeout = timeout
    try:
        yield
    finally:
        self.timeout = original
unlink() -> None

Unlink from the Service.

Source code in src/msl/network/client.py
376
377
378
379
380
381
382
383
def unlink(self) -> None:
    """Unlink from the [Service][]."""
    if self._link_subscriber.interrupter is not None:
        self.unsubscribe()
        self._link_subscriber.interrupter()
        self._client._links.remove(self)  # pyright: ignore[reportPrivateUsage]  # noqa: SLF001
        self._thread.join()
        logger.debug("%s unlinked", self)

unsubscribe ¤

unsubscribe() -> None

Unsubscribe from receiving publications from the linked Service.

Source code in src/msl/network/client.py
385
386
387
388
389
def unsubscribe(self) -> None:
    """Unsubscribe from receiving publications from the linked [Service][]."""
    self._link_subscriber.callback = None
    if self._link_subscriber.sub_socket is not None:
        self._link_subscriber.sub_socket.setsockopt(zmq.UNSUBSCRIBE, self.service_name.encode())