pyobs-core 2.0.0.dev1__py3-none-any.whl → 2.0.0.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyobs/application.py +11 -2
- pyobs/comm/comm.py +88 -4
- pyobs/comm/commlogging.py +5 -2
- pyobs/comm/local/localcomm.py +102 -43
- pyobs/comm/proxy.py +25 -0
- pyobs/comm/xmpp/rpc.py +169 -140
- pyobs/comm/xmpp/serializer.py +338 -0
- pyobs/comm/xmpp/xep_0009_timeout/rpc.py +6 -19
- pyobs/comm/xmpp/xmppcomm.py +230 -98
- pyobs/events/log.py +18 -2
- pyobs/interfaces/IBinning.py +16 -19
- pyobs/interfaces/IConfig.py +6 -24
- pyobs/interfaces/ICooling.py +5 -14
- pyobs/interfaces/IExposure.py +17 -21
- pyobs/interfaces/IExposureTime.py +13 -19
- pyobs/interfaces/IFilters.py +13 -17
- pyobs/interfaces/IFocuser.py +14 -19
- pyobs/interfaces/IGain.py +14 -19
- pyobs/interfaces/IImageFormat.py +15 -19
- pyobs/interfaces/IImageType.py +13 -10
- pyobs/interfaces/IMode.py +13 -32
- pyobs/interfaces/IModule.py +7 -21
- pyobs/interfaces/IMotion.py +18 -13
- pyobs/interfaces/IMultiFiber.py +22 -47
- pyobs/interfaces/IOffsetsAltAz.py +12 -12
- pyobs/interfaces/IOffsetsRaDec.py +12 -12
- pyobs/interfaces/IPointingAltAz.py +12 -10
- pyobs/interfaces/IPointingHGS.py +12 -12
- pyobs/interfaces/IPointingHelioprojective.py +12 -12
- pyobs/interfaces/IPointingRaDec.py +12 -10
- pyobs/interfaces/IReady.py +13 -11
- pyobs/interfaces/IRotation.py +13 -6
- pyobs/interfaces/IRunning.py +13 -1
- pyobs/interfaces/ITemperatures.py +18 -11
- pyobs/interfaces/IVideo.py +7 -10
- pyobs/interfaces/IWindow.py +16 -17
- pyobs/interfaces/__init__.py +48 -24
- pyobs/mixins/fitsheader.py +5 -2
- pyobs/mixins/motionstatus.py +9 -12
- pyobs/mixins/waitformotion.py +6 -3
- pyobs/mixins/weatheraware.py +1 -1
- pyobs/modules/camera/basecamera.py +36 -38
- pyobs/modules/camera/basespectrograph.py +0 -8
- pyobs/modules/camera/basevideo.py +5 -21
- pyobs/modules/camera/dummycamera.py +232 -199
- pyobs/modules/camera/dummyspectrograph.py +0 -11
- pyobs/modules/camera/dummyvideo.py +69 -0
- pyobs/modules/camera/pipelinecamera.py +1 -10
- pyobs/modules/flatfield/flatfield.py +19 -24
- pyobs/modules/module.py +49 -4
- pyobs/modules/pointing/autoguiding.py +2 -12
- pyobs/modules/pointing/scienceframeguiding.py +0 -16
- pyobs/modules/roof/baseroof.py +8 -11
- pyobs/modules/roof/dummyroof.py +3 -0
- pyobs/modules/telescope/basetelescope.py +16 -27
- pyobs/modules/telescope/dummytelescope.py +181 -215
- pyobs/modules/utils/dummymode.py +12 -35
- pyobs/robotic/scripts/control/selector.py +2 -1
- pyobs/robotic/scripts/utils/callmodule.py +39 -17
- pyobs/utils/parallel.py +15 -3
- {pyobs_core-2.0.0.dev1.dist-info → pyobs_core-2.0.0.dev4.dist-info}/METADATA +1 -1
- {pyobs_core-2.0.0.dev1.dist-info → pyobs_core-2.0.0.dev4.dist-info}/RECORD +65 -71
- pyobs/comm/dbus/__init__.py +0 -3
- pyobs/comm/dbus/dbuscomm.py +0 -591
- pyobs/comm/dbus/patch.py +0 -112
- pyobs/interfaces/ILatLon.py +0 -41
- pyobs/utils/simulation/__init__.py +0 -11
- pyobs/utils/simulation/camera.py +0 -284
- pyobs/utils/simulation/telescope.py +0 -200
- pyobs/utils/simulation/world.py +0 -107
- {pyobs_core-2.0.0.dev1.dist-info → pyobs_core-2.0.0.dev4.dist-info}/WHEEL +0 -0
- {pyobs_core-2.0.0.dev1.dist-info → pyobs_core-2.0.0.dev4.dist-info}/entry_points.txt +0 -0
- {pyobs_core-2.0.0.dev1.dist-info → pyobs_core-2.0.0.dev4.dist-info}/licenses/LICENSE +0 -0
pyobs/application.py
CHANGED
|
@@ -140,8 +140,17 @@ class Application:
|
|
|
140
140
|
class_name = cfg["class"]
|
|
141
141
|
klass = get_class_from_string(class_name)
|
|
142
142
|
|
|
143
|
-
# create event loop
|
|
144
|
-
|
|
143
|
+
# create event loop — if top-level class doesn't override new_event_loop,
|
|
144
|
+
# check child modules for one that does (e.g. pyobs_gui.GUI in a MultiModule)
|
|
145
|
+
loop_class = klass
|
|
146
|
+
if klass.new_event_loop is Module.new_event_loop:
|
|
147
|
+
for mod_cfg in cfg.get("modules", {}).values():
|
|
148
|
+
if isinstance(mod_cfg, dict) and "class" in mod_cfg:
|
|
149
|
+
child_klass = get_class_from_string(mod_cfg["class"])
|
|
150
|
+
if child_klass.new_event_loop is not Module.new_event_loop:
|
|
151
|
+
loop_class = child_klass
|
|
152
|
+
break
|
|
153
|
+
self._loop = loop_class.new_event_loop()
|
|
145
154
|
asyncio.set_event_loop(self._loop)
|
|
146
155
|
|
|
147
156
|
# create module and open it
|
pyobs/comm/comm.py
CHANGED
|
@@ -4,12 +4,14 @@ import asyncio
|
|
|
4
4
|
import functools
|
|
5
5
|
import inspect
|
|
6
6
|
import logging
|
|
7
|
+
import sys
|
|
7
8
|
from collections.abc import Callable, Coroutine
|
|
8
9
|
from typing import TYPE_CHECKING, Any, overload
|
|
9
10
|
|
|
10
11
|
import pyobs.interfaces
|
|
11
12
|
from pyobs.events import Event, LogEvent, ModuleClosedEvent
|
|
12
13
|
from pyobs.interfaces import Interface
|
|
14
|
+
from pyobs.utils.enums import ModuleState
|
|
13
15
|
|
|
14
16
|
from .commlogging import CommLoggingHandler
|
|
15
17
|
from .proxy import Proxy, ProxyType, _ProxyContext
|
|
@@ -18,6 +20,7 @@ if TYPE_CHECKING:
|
|
|
18
20
|
from pyobs.modules import Module
|
|
19
21
|
|
|
20
22
|
StateCallback = Callable[[Any], None]
|
|
23
|
+
PresenceCallback = Callable[["ModuleState", str], None]
|
|
21
24
|
|
|
22
25
|
log = logging.getLogger(__name__)
|
|
23
26
|
|
|
@@ -64,9 +67,14 @@ class Comm:
|
|
|
64
67
|
"""Open module."""
|
|
65
68
|
|
|
66
69
|
# add handler to global logger
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
root_logger = logging.getLogger()
|
|
71
|
+
if not any(isinstance(h, CommLoggingHandler) for h in root_logger.handlers):
|
|
72
|
+
from pyobs.utils.logging.context import ModuleNameFilter
|
|
73
|
+
|
|
74
|
+
handler = CommLoggingHandler(self)
|
|
75
|
+
handler.setLevel(logging.INFO)
|
|
76
|
+
handler.addFilter(ModuleNameFilter())
|
|
77
|
+
root_logger.addHandler(handler)
|
|
70
78
|
|
|
71
79
|
# start logging thread
|
|
72
80
|
self._logging_task = asyncio.create_task(self._logging())
|
|
@@ -129,7 +137,7 @@ class Comm:
|
|
|
129
137
|
|
|
130
138
|
# subscribe to state
|
|
131
139
|
for interface in interfaces:
|
|
132
|
-
if getattr(interface, "
|
|
140
|
+
if getattr(interface, "State", None) is not None:
|
|
133
141
|
await self.subscribe_state(client, interface, functools.partial(proxy.update_state, interface))
|
|
134
142
|
|
|
135
143
|
self._proxies[client] = proxy
|
|
@@ -442,6 +450,68 @@ class Comm:
|
|
|
442
450
|
async def _set_state(self, interface: type[Interface], state: Any) -> None:
|
|
443
451
|
pass
|
|
444
452
|
|
|
453
|
+
@staticmethod
|
|
454
|
+
def _interface_from_capabilities(caps_cls: type) -> type:
|
|
455
|
+
outer_name = caps_cls.__qualname__.rsplit(".", 1)[0]
|
|
456
|
+
return getattr(sys.modules[caps_cls.__module__], outer_name)
|
|
457
|
+
|
|
458
|
+
async def set_capabilities(self, capabilities: Any) -> None:
|
|
459
|
+
"""Publish capabilities for this module.
|
|
460
|
+
|
|
461
|
+
Called by Module.open() for each interface that defines a Capabilities
|
|
462
|
+
dataclass. Not intended to be called directly by module authors after
|
|
463
|
+
that point — capabilities are fixed for the module lifetime.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
capabilities: Capabilities dataclass instance.
|
|
467
|
+
"""
|
|
468
|
+
interface = Comm._interface_from_capabilities(type(capabilities))
|
|
469
|
+
await self._set_capabilities(interface, capabilities)
|
|
470
|
+
|
|
471
|
+
async def _set_capabilities(self, interface: type[Interface], capabilities: Any) -> None:
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
async def get_capabilities(self, module: str, interface: type[Interface]) -> Any | None:
|
|
475
|
+
"""Fetch and deserialize capabilities for a remote module's interface.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
module: Module name (e.g. "camera").
|
|
479
|
+
interface: Interface class whose Capabilities dataclass to fetch.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Deserialized Capabilities dataclass instance, or None if not published.
|
|
483
|
+
"""
|
|
484
|
+
return await self._get_capabilities(module, interface)
|
|
485
|
+
|
|
486
|
+
async def _get_capabilities(self, module: str, interface: type[Interface]) -> Any | None:
|
|
487
|
+
return None
|
|
488
|
+
|
|
489
|
+
async def set_presence(self, state: ModuleState, error_string: str = "") -> None:
|
|
490
|
+
"""Publish presence for this module (module lifecycle state).
|
|
491
|
+
|
|
492
|
+
Called automatically by Module.set_state — not intended to be called
|
|
493
|
+
directly by module authors.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
state: Current module lifecycle state.
|
|
497
|
+
error_string: Error message, used when state is ERROR.
|
|
498
|
+
"""
|
|
499
|
+
await self._set_presence(state, error_string)
|
|
500
|
+
|
|
501
|
+
async def _set_presence(self, state: ModuleState, error_string: str = "") -> None:
|
|
502
|
+
pass
|
|
503
|
+
|
|
504
|
+
def get_client_state(self, module: str) -> tuple[ModuleState, str] | None:
|
|
505
|
+
"""Return the last known presence state of a connected module.
|
|
506
|
+
|
|
507
|
+
Returns (ModuleState, error_string) or None if the module is not connected.
|
|
508
|
+
This replaces the old get_state()/get_error_string() RPC pattern.
|
|
509
|
+
"""
|
|
510
|
+
return self._get_client_state(module)
|
|
511
|
+
|
|
512
|
+
def _get_client_state(self, module: str) -> tuple[ModuleState, str] | None:
|
|
513
|
+
return None
|
|
514
|
+
|
|
445
515
|
async def subscribe_state(
|
|
446
516
|
self,
|
|
447
517
|
module: str,
|
|
@@ -491,6 +561,20 @@ class Comm:
|
|
|
491
561
|
) -> None:
|
|
492
562
|
pass
|
|
493
563
|
|
|
564
|
+
async def subscribe_presence(self, module: str, callback: PresenceCallback) -> None:
|
|
565
|
+
"""Subscribe to presence updates for a given module.
|
|
566
|
+
|
|
567
|
+
Delivers the current value immediately, then on every change.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
module: Name of remote module.
|
|
571
|
+
callback: Called with (ModuleState, error_string) on each update.
|
|
572
|
+
"""
|
|
573
|
+
await self._subscribe_presence(module, callback)
|
|
574
|
+
|
|
575
|
+
async def _subscribe_presence(self, module: str, callback: PresenceCallback) -> None:
|
|
576
|
+
pass
|
|
577
|
+
|
|
494
578
|
def _send_event_to_module(self, event: Event, from_client: str) -> None:
|
|
495
579
|
"""Send an event to all connected modules.
|
|
496
580
|
|
pyobs/comm/commlogging.py
CHANGED
|
@@ -34,9 +34,12 @@ class CommLoggingHandler(logging.Handler):
|
|
|
34
34
|
# format message
|
|
35
35
|
msg = self._formatter.format(rec) # noqa: UP031
|
|
36
36
|
|
|
37
|
-
# create and send event
|
|
37
|
+
# create and send event — use pyobs_module if set (MultiModule context)
|
|
38
38
|
time = datetime.fromtimestamp(rec.created, tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")
|
|
39
|
-
|
|
39
|
+
sender = getattr(rec, "pyobs_module", "") or ""
|
|
40
|
+
entry = LogEvent(
|
|
41
|
+
time, rec.levelname, os.path.basename(rec.pathname), rec.funcName, rec.lineno, msg, sender=sender
|
|
42
|
+
)
|
|
40
43
|
self._comm.log_message(entry)
|
|
41
44
|
|
|
42
45
|
|
pyobs/comm/local/localcomm.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections.abc import Callable, Coroutine
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
4
6
|
from pyobs.comm import Comm
|
|
7
|
+
from pyobs.comm.comm import PresenceCallback
|
|
5
8
|
from pyobs.events import Event
|
|
6
9
|
from pyobs.interfaces import Interface
|
|
10
|
+
from pyobs.utils.enums import ModuleState
|
|
7
11
|
from pyobs.utils.types import cast_response_to_real
|
|
8
12
|
|
|
9
13
|
from .localnetwork import LocalNetwork
|
|
@@ -17,6 +21,13 @@ class LocalComm(Comm):
|
|
|
17
21
|
self._network = LocalNetwork()
|
|
18
22
|
self._network.connect_client(self)
|
|
19
23
|
|
|
24
|
+
# in-memory state and capabilities storage
|
|
25
|
+
self._states: dict[str, Any] = {} # node -> state object
|
|
26
|
+
self._state_handlers: dict[str, list[tuple[type[Interface], Callable[[Any], None]]]] = {}
|
|
27
|
+
self._capabilities: dict[type[Interface], Any] = {} # interface -> Capabilities object
|
|
28
|
+
self._presence: tuple[ModuleState, str] = (ModuleState.READY, "")
|
|
29
|
+
self._presence_callbacks: dict[str, list[PresenceCallback]] = {}
|
|
30
|
+
|
|
20
31
|
@property
|
|
21
32
|
def name(self) -> str:
|
|
22
33
|
"""Name of this client."""
|
|
@@ -24,54 +35,21 @@ class LocalComm(Comm):
|
|
|
24
35
|
|
|
25
36
|
@property
|
|
26
37
|
def clients(self) -> list[str]:
|
|
27
|
-
"""Returns list of currently connected clients.
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
(list) List of currently connected clients.
|
|
31
|
-
"""
|
|
38
|
+
"""Returns list of currently connected clients."""
|
|
32
39
|
return self._network.get_client_names()
|
|
33
40
|
|
|
34
41
|
async def get_interfaces(self, client: str) -> list[type[Interface]]:
|
|
35
|
-
"""Returns list of interfaces for given client.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
client: Name of client.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
List of supported interfaces.
|
|
42
|
-
|
|
43
|
-
Raises:
|
|
44
|
-
IndexError: If client cannot be found.
|
|
45
|
-
"""
|
|
42
|
+
"""Returns list of interfaces for given client."""
|
|
46
43
|
remote_client = self._network.get_client(client)
|
|
47
44
|
return [] if not remote_client.has_module else remote_client.module.interfaces
|
|
48
45
|
|
|
49
46
|
async def _supports_interface(self, client: str, interface: type[Interface]) -> bool:
|
|
50
|
-
"""Checks
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
client: Client to check.
|
|
54
|
-
interface: Interface to check.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
Whether interface is supported.
|
|
58
|
-
"""
|
|
47
|
+
"""Checks whether the given client supports the given interface."""
|
|
59
48
|
interfaces = await self.get_interfaces(client)
|
|
60
49
|
return interface in interfaces
|
|
61
50
|
|
|
62
51
|
async def execute(self, client: str, method: str, annotation: dict[str, Any], *args: Any) -> Any:
|
|
63
|
-
"""Execute a given method on a remote client.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
client (str): ID of client.
|
|
67
|
-
method (str): Method to call.
|
|
68
|
-
annotation: Method annotation.
|
|
69
|
-
*args: List of parameters for given method.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
Passes through return from method call.
|
|
73
|
-
"""
|
|
74
|
-
|
|
52
|
+
"""Execute a given method on a remote client."""
|
|
75
53
|
remote_client = self._network.get_client(client)
|
|
76
54
|
if remote_client.module is None:
|
|
77
55
|
raise ValueError
|
|
@@ -82,17 +60,98 @@ class LocalComm(Comm):
|
|
|
82
60
|
return real_results
|
|
83
61
|
|
|
84
62
|
async def send_event(self, event: Event) -> None:
|
|
85
|
-
"""Send an event to other clients.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
event (Event): Event to send
|
|
89
|
-
"""
|
|
63
|
+
"""Send an event to other clients."""
|
|
64
|
+
from pyobs.events import LogEvent as _LogEvent
|
|
90
65
|
|
|
66
|
+
sender = self.name
|
|
67
|
+
if isinstance(event, _LogEvent) and event.sender:
|
|
68
|
+
sender = event.sender
|
|
91
69
|
remote_clients = self._network.get_clients()
|
|
92
70
|
for client in remote_clients:
|
|
93
|
-
client._send_event_to_module(event,
|
|
71
|
+
client._send_event_to_module(event, sender)
|
|
94
72
|
|
|
95
73
|
async def _register_events(
|
|
96
74
|
self, events: list[type[Event]], handler: Callable[[Event, str], Coroutine[Any, Any, bool]] | None = None
|
|
97
75
|
) -> None:
|
|
98
76
|
pass
|
|
77
|
+
|
|
78
|
+
# -------------------------------------------------------------------------
|
|
79
|
+
# State
|
|
80
|
+
# -------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
async def _set_state(self, interface: type[Interface], state: Any) -> None:
|
|
83
|
+
"""Publish state locally and dispatch to subscribers."""
|
|
84
|
+
node = f"pyobs:state:{self._name}:{interface.__name__}:{interface.version}"
|
|
85
|
+
self._states[node] = state
|
|
86
|
+
|
|
87
|
+
# dispatch to all subscribers for this node
|
|
88
|
+
if node in self._state_handlers:
|
|
89
|
+
for _, callback in self._state_handlers[node]:
|
|
90
|
+
callback(state)
|
|
91
|
+
|
|
92
|
+
async def _subscribe_state(self, module: str, interface: type[Interface], callback: Callable[[Any], None]) -> None:
|
|
93
|
+
"""Subscribe to state updates from a remote module."""
|
|
94
|
+
node = f"pyobs:state:{module}:{interface.__name__}:{interface.version}"
|
|
95
|
+
|
|
96
|
+
if node not in self._state_handlers:
|
|
97
|
+
self._state_handlers[node] = []
|
|
98
|
+
self._state_handlers[node].append((interface, callback))
|
|
99
|
+
|
|
100
|
+
# also register on the remote comm so it can dispatch to us
|
|
101
|
+
try:
|
|
102
|
+
remote = self._network.get_client(module)
|
|
103
|
+
if node not in remote._state_handlers:
|
|
104
|
+
remote._state_handlers[node] = []
|
|
105
|
+
remote._state_handlers[node].append((interface, callback))
|
|
106
|
+
|
|
107
|
+
# deliver current value immediately if available
|
|
108
|
+
if node in remote._states:
|
|
109
|
+
callback(remote._states[node])
|
|
110
|
+
except KeyError:
|
|
111
|
+
pass # remote not connected yet
|
|
112
|
+
|
|
113
|
+
# -------------------------------------------------------------------------
|
|
114
|
+
# Capabilities
|
|
115
|
+
# -------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
async def _set_capabilities(self, interface: type[Interface], capabilities: Any) -> None:
|
|
118
|
+
"""Store capabilities locally."""
|
|
119
|
+
self._capabilities[interface] = capabilities
|
|
120
|
+
|
|
121
|
+
async def _get_capabilities(self, module: str, interface: type[Interface]) -> Any | None:
|
|
122
|
+
"""Fetch capabilities from a remote module."""
|
|
123
|
+
if not hasattr(interface, "Capabilities"):
|
|
124
|
+
return None
|
|
125
|
+
try:
|
|
126
|
+
remote = self._network.get_client(module)
|
|
127
|
+
return remote._capabilities.get(interface)
|
|
128
|
+
except KeyError:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# -------------------------------------------------------------------------
|
|
132
|
+
# Presence
|
|
133
|
+
# -------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
async def _set_presence(self, state: ModuleState, error_string: str = "") -> None:
|
|
136
|
+
"""Store presence state and dispatch to all subscribers."""
|
|
137
|
+
self._presence = (state, error_string)
|
|
138
|
+
for client in self._network.get_clients():
|
|
139
|
+
for cb in client._presence_callbacks.get(self._name, []):
|
|
140
|
+
cb(state, error_string)
|
|
141
|
+
|
|
142
|
+
def _get_client_state(self, module: str) -> tuple[ModuleState, str] | None:
|
|
143
|
+
"""Return presence state of a connected module."""
|
|
144
|
+
try:
|
|
145
|
+
remote = self._network.get_client(module)
|
|
146
|
+
return remote._presence
|
|
147
|
+
except KeyError:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
async def _subscribe_presence(self, module: str, callback: PresenceCallback) -> None:
|
|
151
|
+
"""Register a presence callback and deliver the current state immediately."""
|
|
152
|
+
self._presence_callbacks.setdefault(module, []).append(callback)
|
|
153
|
+
try:
|
|
154
|
+
remote = self._network.get_client(module)
|
|
155
|
+
callback(*remote._presence)
|
|
156
|
+
except KeyError:
|
|
157
|
+
pass
|
pyobs/comm/proxy.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import inspect
|
|
4
5
|
import types
|
|
5
6
|
from collections.abc import Coroutine
|
|
@@ -169,6 +170,30 @@ class Proxy:
|
|
|
169
170
|
"""Latest known state for the given interface, or None if nothing has arrived yet."""
|
|
170
171
|
return self._state.get(interface)
|
|
171
172
|
|
|
173
|
+
async def wait_for_state(
|
|
174
|
+
self,
|
|
175
|
+
interface: type[Interface],
|
|
176
|
+
timeout: float = 10.0,
|
|
177
|
+
) -> Any:
|
|
178
|
+
"""Return state immediately if available, otherwise wait for the first update."""
|
|
179
|
+
if self._state.get(interface) is not None:
|
|
180
|
+
return self._state[interface]
|
|
181
|
+
|
|
182
|
+
event = asyncio.Event()
|
|
183
|
+
|
|
184
|
+
def _notify(state: Any) -> None:
|
|
185
|
+
self._state[interface] = state
|
|
186
|
+
event.set()
|
|
187
|
+
|
|
188
|
+
# patch in a one-shot notifier alongside the existing callback
|
|
189
|
+
await self._comm.subscribe_state(self._client, interface, _notify)
|
|
190
|
+
try:
|
|
191
|
+
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
192
|
+
finally:
|
|
193
|
+
await self._comm.unsubscribe_state(self._client, interface, _notify)
|
|
194
|
+
|
|
195
|
+
return self._state.get(interface)
|
|
196
|
+
|
|
172
197
|
|
|
173
198
|
class _ProxyContext(Generic[ProxyType]):
|
|
174
199
|
"""Returned by Comm.proxy() / Object.proxy() / Comm.safe_proxy(). Must be used as:
|