quelware-client 0.2.0__tar.gz → 0.3.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.
- {quelware_client-0.2.0 → quelware_client-0.3.0}/CHANGELOG.md +17 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/PKG-INFO +2 -2
- {quelware_client-0.2.0 → quelware_client-0.3.0}/pyproject.toml +2 -2
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/_grpc.py +18 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/_standalone_grpc.py +7 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/_agent_container.py +28 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/_client.py +5 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/_session.py +48 -22
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/exceptions/__init__.py +4 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/instrument_agent/__init__.py +0 -5
- quelware_client-0.3.0/src/quelware_client/core/interfaces/trigger_agent/__init__.py +18 -0
- quelware_client-0.3.0/src/quelware_client/core/interfaces/worker_agent/__init__.py +7 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/instrument_agent_grpc/__init__.py +0 -11
- quelware_client-0.3.0/src/quelware_client/infra/trigger_agent_grpc/__init__.py +52 -0
- quelware_client-0.3.0/src/quelware_client/infra/worker_agent_grpc/__init__.py +24 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/testing/instrument_agent_mock/__init__.py +0 -6
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/testing/mock_client.py +7 -0
- quelware_client-0.3.0/src/quelware_client/testing/trigger_agent_mock/__init__.py +24 -0
- quelware_client-0.3.0/src/quelware_client/testing/worker_agent_mock/__init__.py +12 -0
- quelware_client-0.3.0/tests/core/session/test_trigger.py +38 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/uv.lock +2 -2
- {quelware_client-0.2.0 → quelware_client-0.3.0}/.gitignore +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/LICENSE +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/Makefile +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/README.md +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/examples/generate_readout_pulse.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/cli/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/cli/quel3_echo_test.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/helpers/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/helpers/instrument_resolver/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/helpers/sequencer/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/_utils.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/instrument_driver/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/diagnostics_agent/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/health_agent/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/pat_provider/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/resource_agent/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/session_agent/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/system_configuration_agent/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/trigger_count_proposer/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/_grpc_retry.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/diagnostics_agent_grpc/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/health_agent_grpc/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/pat_provider_file/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/resource_agent_grpc/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/session_agent_grpc/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/infra/system_configuration_agent_grpc/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/py.typed +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/testing/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/testing/resource_agent_mock/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/testing/session_agent_mock/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/testing/system_configuration_agent_mock/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/client/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/client/helpers/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/client/helpers/test_instrument_resolver.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/client/helpers/test_sequencer.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/client/test_client.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/instrument_driver/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/instrument_driver/test_instrument_driver.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/session/__init__.py +0 -0
- {quelware_client-0.2.0 → quelware_client-0.3.0}/tests/core/session/test_session.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Session.trigger` now routes through the manager by default (single round-trip, shorter lead).
|
|
10
|
+
- `ServiceUnavailableError` exception for catching cases where the server-side service is not available.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `Session.trigger` default `wait_ms` changed from `400` to `None`.
|
|
15
|
+
- `Session.trigger` returns the scheduled clock count (`int`).
|
|
16
|
+
- Default multi-unit trigger offset is 0 ticks (was 16).
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- `agent.instrument(unit).get_clock_snapshot()` — use `agent.worker(unit).get_clock_snapshot()`.
|
|
21
|
+
|
|
5
22
|
## [0.2.0] - 2026-05-28
|
|
6
23
|
|
|
7
24
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quelware-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Project-URL: Repository, https://github.com/quel-inc/quelware-client
|
|
5
5
|
Project-URL: Documentation, https://quel-inc.github.io/quelware-client/
|
|
6
6
|
Project-URL: Changelog, https://github.com/quel-inc/quelware-client/blob/main/quelware-client/CHANGELOG.md
|
|
@@ -10,7 +10,7 @@ License-File: LICENSE
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: betterproto2>=0.9.1
|
|
12
12
|
Requires-Dist: grpclib>=0.4.8
|
|
13
|
-
Requires-Dist: quelware-core>=0.
|
|
13
|
+
Requires-Dist: quelware-core>=0.2.0
|
|
14
14
|
Requires-Dist: typer>=0.21.1
|
|
15
15
|
Requires-Dist: typing-extensions>=4.1.0
|
|
16
16
|
Provides-Extra: plot
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quelware-client"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -9,7 +9,7 @@ authors = [
|
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"betterproto2>=0.9.1",
|
|
12
|
-
"quelware-core>=0.
|
|
12
|
+
"quelware-core>=0.2.0",
|
|
13
13
|
"grpclib>=0.4.8",
|
|
14
14
|
"typer>=0.21.1",
|
|
15
15
|
"typing_extensions>=4.1.0"
|
|
@@ -13,6 +13,7 @@ from quelware_client.core.interfaces.session_agent import SessionAgent
|
|
|
13
13
|
from quelware_client.core.interfaces.system_configuration_agent import (
|
|
14
14
|
SystemConfigurationAgent,
|
|
15
15
|
)
|
|
16
|
+
from quelware_client.core.interfaces.worker_agent import WorkerAgent
|
|
16
17
|
from quelware_client.infra.diagnostics_agent_grpc import DiagnosticsAgentGrpc
|
|
17
18
|
from quelware_client.infra.health_agent_grpc import HealthAgentGrpc
|
|
18
19
|
from quelware_client.infra.instrument_agent_grpc import InstrumentAgentGrpc
|
|
@@ -24,6 +25,8 @@ from quelware_client.infra.session_agent_grpc import SessionAgentGrpc
|
|
|
24
25
|
from quelware_client.infra.system_configuration_agent_grpc import (
|
|
25
26
|
SystemConfigurationAgentGrpc,
|
|
26
27
|
)
|
|
28
|
+
from quelware_client.infra.trigger_agent_grpc import TriggerAgentGrpc
|
|
29
|
+
from quelware_client.infra.worker_agent_grpc import WorkerAgentGrpc
|
|
27
30
|
|
|
28
31
|
logger = logging.getLogger(__name__)
|
|
29
32
|
|
|
@@ -64,6 +67,13 @@ def _create_default_diagnostics_agent_factory(channel, pat: str):
|
|
|
64
67
|
return _default_diagnostics_agent_factory
|
|
65
68
|
|
|
66
69
|
|
|
70
|
+
def _create_default_worker_agent_factory(channel):
|
|
71
|
+
def _default_worker_agent_factory(ul: UnitLabel):
|
|
72
|
+
return WorkerAgentGrpc(channel, metadata={"x-unit-label": str(ul)})
|
|
73
|
+
|
|
74
|
+
return _default_worker_agent_factory
|
|
75
|
+
|
|
76
|
+
|
|
67
77
|
_CENTRAL_SERVER_METADATA_BASE = {"x-unit-label": "central-server"}
|
|
68
78
|
|
|
69
79
|
|
|
@@ -76,6 +86,7 @@ def create_quelware_client( # noqa: PLR0913
|
|
|
76
86
|
resource_agent_factory: AgentFactory[ResourceAgent] | None = None,
|
|
77
87
|
instrument_agent_factory: AgentFactory[InstrumentAgent] | None = None,
|
|
78
88
|
diagnostics_agent_factory: AgentFactory[DiagnosticsAgent] | None = None,
|
|
89
|
+
worker_agent_factory: AgentFactory[WorkerAgent] | None = None,
|
|
79
90
|
pat: PatProvider | str | None = None,
|
|
80
91
|
):
|
|
81
92
|
channel = Channel(endpoint, port)
|
|
@@ -105,6 +116,9 @@ def create_quelware_client( # noqa: PLR0913
|
|
|
105
116
|
channel, _pat
|
|
106
117
|
)
|
|
107
118
|
|
|
119
|
+
if worker_agent_factory is None:
|
|
120
|
+
worker_agent_factory = _create_default_worker_agent_factory(channel)
|
|
121
|
+
|
|
108
122
|
central_server_metadata = _CENTRAL_SERVER_METADATA_BASE | {"x-pat": _pat}
|
|
109
123
|
|
|
110
124
|
agent_container = AgentContainer()
|
|
@@ -116,11 +130,15 @@ def create_quelware_client( # noqa: PLR0913
|
|
|
116
130
|
agent_container.system_configuration = SystemConfigurationAgentGrpc(
|
|
117
131
|
channel, metadata=central_server_metadata
|
|
118
132
|
)
|
|
133
|
+
agent_container.trigger = TriggerAgentGrpc(
|
|
134
|
+
channel, metadata=central_server_metadata
|
|
135
|
+
)
|
|
119
136
|
return QuelwareClient(
|
|
120
137
|
agent=agent_container,
|
|
121
138
|
health_agent_factory=health_agent_factory,
|
|
122
139
|
resource_agent_factory=resource_agent_factory,
|
|
123
140
|
instrument_agent_factory=instrument_agent_factory,
|
|
124
141
|
diagnostics_agent_factory=diagnostics_agent_factory,
|
|
142
|
+
worker_agent_factory=worker_agent_factory,
|
|
125
143
|
close_handlers=[channel.close],
|
|
126
144
|
)
|
{quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/_standalone_grpc.py
RENAMED
|
@@ -9,6 +9,8 @@ from quelware_client.infra.diagnostics_agent_grpc import DiagnosticsAgentGrpc
|
|
|
9
9
|
from quelware_client.infra.instrument_agent_grpc import InstrumentAgentGrpc
|
|
10
10
|
from quelware_client.infra.pat_provider_file import pat_provider_from_config
|
|
11
11
|
from quelware_client.infra.resource_agent_grpc import ResourceAgentGrpc
|
|
12
|
+
from quelware_client.infra.trigger_agent_grpc import TriggerAgentGrpc
|
|
13
|
+
from quelware_client.infra.worker_agent_grpc import WorkerAgentGrpc
|
|
12
14
|
from quelware_client.testing.session_agent_mock import SessionAgentMock
|
|
13
15
|
from quelware_client.testing.system_configuration_agent_mock import (
|
|
14
16
|
SystemConfigurationAgentMock,
|
|
@@ -59,9 +61,13 @@ def create_standalone_client(
|
|
|
59
61
|
channel, metadata={"x-unit-label": str(ul), "x-pat": _pat}
|
|
60
62
|
)
|
|
61
63
|
|
|
64
|
+
def worker_agent_factory(ul: UnitLabel):
|
|
65
|
+
return WorkerAgentGrpc(channel, metadata={"x-unit-label": str(ul)})
|
|
66
|
+
|
|
62
67
|
agent_container = AgentContainer()
|
|
63
68
|
agent_container.session = session_agent
|
|
64
69
|
agent_container.system_configuration = conf_agent
|
|
70
|
+
agent_container.trigger = TriggerAgentGrpc(channel)
|
|
65
71
|
|
|
66
72
|
logger.warning("NOTE: Standalone client is for testing purposes.")
|
|
67
73
|
|
|
@@ -70,6 +76,7 @@ def create_standalone_client(
|
|
|
70
76
|
resource_agent_factory=resource_agent_factory,
|
|
71
77
|
instrument_agent_factory=instrument_agent_factory,
|
|
72
78
|
diagnostics_agent_factory=diagnostics_agent_factory,
|
|
79
|
+
worker_agent_factory=worker_agent_factory,
|
|
73
80
|
close_handlers=[channel.close],
|
|
74
81
|
skip_lock_check=skip_lock_check,
|
|
75
82
|
)
|
{quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/_agent_container.py
RENAMED
|
@@ -10,6 +10,8 @@ from quelware_client.core.interfaces.session_agent import SessionAgent
|
|
|
10
10
|
from quelware_client.core.interfaces.system_configuration_agent import (
|
|
11
11
|
SystemConfigurationAgent,
|
|
12
12
|
)
|
|
13
|
+
from quelware_client.core.interfaces.trigger_agent import TriggerAgent
|
|
14
|
+
from quelware_client.core.interfaces.worker_agent import WorkerAgent
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
@@ -18,10 +20,12 @@ class AgentContainer:
|
|
|
18
20
|
def __init__(self):
|
|
19
21
|
self._session: SessionAgent | None = None
|
|
20
22
|
self._conf: SystemConfigurationAgent | None = None
|
|
23
|
+
self._trigger: TriggerAgent | None = None
|
|
21
24
|
self._health: dict[UnitLabel, HealthAgent] = {}
|
|
22
25
|
self._rsrc: dict[UnitLabel, ResourceAgent] = {}
|
|
23
26
|
self._inst: dict[UnitLabel, InstrumentAgent] = {}
|
|
24
27
|
self._diag: dict[UnitLabel, DiagnosticsAgent] = {}
|
|
28
|
+
self._worker: dict[UnitLabel, WorkerAgent] = {}
|
|
25
29
|
|
|
26
30
|
def update_health_agent(self, unit_label: UnitLabel, agent: HealthAgent | None):
|
|
27
31
|
if agent is None:
|
|
@@ -67,6 +71,15 @@ class AgentContainer:
|
|
|
67
71
|
else:
|
|
68
72
|
self._diag[unit_label] = agent
|
|
69
73
|
|
|
74
|
+
def update_worker_agent(self, unit_label: UnitLabel, agent: WorkerAgent | None):
|
|
75
|
+
if agent is None:
|
|
76
|
+
if unit_label in self._worker:
|
|
77
|
+
self._worker[unit_label]
|
|
78
|
+
else:
|
|
79
|
+
logger.warning(f"WorkerAgent for '{unit_label}' not found. Continue.")
|
|
80
|
+
else:
|
|
81
|
+
self._worker[unit_label] = agent
|
|
82
|
+
|
|
70
83
|
@property
|
|
71
84
|
def session(self) -> SessionAgent:
|
|
72
85
|
if self._session is None:
|
|
@@ -87,6 +100,16 @@ class AgentContainer:
|
|
|
87
100
|
def system_configuration(self, val: SystemConfigurationAgent):
|
|
88
101
|
self._conf = val
|
|
89
102
|
|
|
103
|
+
@property
|
|
104
|
+
def trigger(self) -> TriggerAgent:
|
|
105
|
+
if self._trigger is None:
|
|
106
|
+
raise ValueError("TriggerAgent not set.")
|
|
107
|
+
return self._trigger
|
|
108
|
+
|
|
109
|
+
@trigger.setter
|
|
110
|
+
def trigger(self, val: TriggerAgent):
|
|
111
|
+
self._trigger = val
|
|
112
|
+
|
|
90
113
|
def health(self, unit_label: UnitLabel) -> HealthAgent:
|
|
91
114
|
if unit_label not in self._health:
|
|
92
115
|
raise ValueError(f"HealthAgent for '{unit_label}' not set.")
|
|
@@ -106,3 +129,8 @@ class AgentContainer:
|
|
|
106
129
|
if unit_label not in self._diag:
|
|
107
130
|
raise ValueError(f"DiagnosticsAgent for '{unit_label}' not set.")
|
|
108
131
|
return self._diag[unit_label]
|
|
132
|
+
|
|
133
|
+
def worker(self, unit_label: UnitLabel) -> WorkerAgent:
|
|
134
|
+
if unit_label not in self._worker:
|
|
135
|
+
raise ValueError(f"WorkerAgent for '{unit_label}' not set.")
|
|
136
|
+
return self._worker[unit_label]
|
|
@@ -16,6 +16,7 @@ from quelware_client.core._agent_container import AgentContainer
|
|
|
16
16
|
from quelware_client.core.interfaces.diagnostics_agent import DiagnosticsAgent
|
|
17
17
|
from quelware_client.core.interfaces.health_agent import HealthAgent
|
|
18
18
|
from quelware_client.core.interfaces.instrument_agent import InstrumentAgent
|
|
19
|
+
from quelware_client.core.interfaces.worker_agent import WorkerAgent
|
|
19
20
|
|
|
20
21
|
from ._session import Session
|
|
21
22
|
from .interfaces.resource_agent import ResourceAgent
|
|
@@ -35,6 +36,7 @@ class QuelwareClient:
|
|
|
35
36
|
resource_agent_factory: AgentFactory[ResourceAgent] | None = None,
|
|
36
37
|
instrument_agent_factory: AgentFactory[InstrumentAgent] | None = None,
|
|
37
38
|
diagnostics_agent_factory: AgentFactory[DiagnosticsAgent] | None = None,
|
|
39
|
+
worker_agent_factory: AgentFactory[WorkerAgent] | None = None,
|
|
38
40
|
close_handlers: list[Callable[[], None]] | None = None,
|
|
39
41
|
skip_lock_check: bool = False,
|
|
40
42
|
):
|
|
@@ -43,6 +45,7 @@ class QuelwareClient:
|
|
|
43
45
|
self._rsrc_agent_factory = resource_agent_factory
|
|
44
46
|
self._inst_agent_factory = instrument_agent_factory
|
|
45
47
|
self._diag_agent_factory = diagnostics_agent_factory
|
|
48
|
+
self._worker_agent_factory = worker_agent_factory
|
|
46
49
|
self._unit_labels: list[UnitLabel] = []
|
|
47
50
|
self._close_handlers = close_handlers or []
|
|
48
51
|
self._skip_lock_check = skip_lock_check
|
|
@@ -91,6 +94,8 @@ class QuelwareClient:
|
|
|
91
94
|
self._agent.update_instrument_agent(ul, self._inst_agent_factory(ul))
|
|
92
95
|
if self._diag_agent_factory:
|
|
93
96
|
self._agent.update_diagnostics_agent(ul, self._diag_agent_factory(ul))
|
|
97
|
+
if self._worker_agent_factory:
|
|
98
|
+
self._agent.update_worker_agent(ul, self._worker_agent_factory(ul))
|
|
94
99
|
self._unit_labels = healthy_labels
|
|
95
100
|
|
|
96
101
|
def list_unit_labels(self) -> list[UnitLabel]:
|
|
@@ -14,6 +14,7 @@ from quelware_core.entities.session import SessionToken
|
|
|
14
14
|
from quelware_core.entities.unit import UnitLabel
|
|
15
15
|
|
|
16
16
|
from quelware_client.core import AgentContainer
|
|
17
|
+
from quelware_client.core.exceptions import ServiceUnavailableError
|
|
17
18
|
from quelware_client.core.trigger_count_proposer import (
|
|
18
19
|
FixedOffsetTriggerCountProposer,
|
|
19
20
|
TriggerCountProposer,
|
|
@@ -23,8 +24,9 @@ from ._utils import create_unit_to_ids_map
|
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
26
|
-
_default_count_proposer = FixedOffsetTriggerCountProposer(grid_step=32, offset=
|
|
27
|
+
_default_count_proposer = FixedOffsetTriggerCountProposer(grid_step=32, offset=0)
|
|
27
28
|
_CLOCK_FREQUENCY_HZ = 312_500_000
|
|
29
|
+
_FALLBACK_MIN_WAIT_MS = 500
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class Session:
|
|
@@ -143,7 +145,11 @@ class Session:
|
|
|
143
145
|
)
|
|
144
146
|
return insts
|
|
145
147
|
|
|
146
|
-
async def trigger(
|
|
148
|
+
async def trigger(
|
|
149
|
+
self,
|
|
150
|
+
instrument_ids: Collection[ResourceId],
|
|
151
|
+
wait_ms: int | None = None,
|
|
152
|
+
) -> int:
|
|
147
153
|
unit_to_ids = create_unit_to_ids_map(instrument_ids)
|
|
148
154
|
|
|
149
155
|
logger.info(f"starting application (token= {self.token} )")
|
|
@@ -154,27 +160,47 @@ class Session:
|
|
|
154
160
|
await asyncio.gather(*apply_coros)
|
|
155
161
|
logger.info(f"finished application (token= {self.token} )")
|
|
156
162
|
|
|
163
|
+
try:
|
|
164
|
+
scheduled = await self._agent.trigger.trigger(
|
|
165
|
+
self.token,
|
|
166
|
+
list(instrument_ids),
|
|
167
|
+
requested_min_wait_ms=wait_ms,
|
|
168
|
+
)
|
|
169
|
+
logger.info(f"trigger scheduled via manager at clock_count={scheduled}")
|
|
170
|
+
return scheduled
|
|
171
|
+
except ServiceUnavailableError:
|
|
172
|
+
fallback_wait_ms = max(wait_ms or 0, _FALLBACK_MIN_WAIT_MS)
|
|
173
|
+
logger.warning(
|
|
174
|
+
f"Manager-side TriggerService unavailable; falling back to "
|
|
175
|
+
f"client-side trigger (wait_ms={fallback_wait_ms})."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return await self._client_side_trigger_fallback(unit_to_ids, fallback_wait_ms)
|
|
179
|
+
|
|
180
|
+
async def _client_side_trigger_fallback(
|
|
181
|
+
self,
|
|
182
|
+
unit_to_ids: dict[UnitLabel, list[ResourceId]],
|
|
183
|
+
wait_ms: int,
|
|
184
|
+
) -> int:
|
|
157
185
|
if len(unit_to_ids) == 1:
|
|
158
186
|
unit_label = next(iter(unit_to_ids))
|
|
159
|
-
logger.info(f"
|
|
187
|
+
logger.info(f"fallback: self-timed trigger (unit={unit_label})")
|
|
160
188
|
scheduled = await self._agent.instrument(unit_label).trigger_now(self.token)
|
|
161
189
|
logger.info(f"trigger scheduled at clock_count={scheduled}")
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await asyncio.gather(*trigger_coros)
|
|
180
|
-
logger.info(f"trigger scheduled at clock_count={target_time}")
|
|
190
|
+
return scheduled
|
|
191
|
+
|
|
192
|
+
logger.info(
|
|
193
|
+
f"fallback: multi-unit sync trigger "
|
|
194
|
+
f"(n_units={len(unit_to_ids)}, wait_ms={wait_ms})"
|
|
195
|
+
)
|
|
196
|
+
reference_unit = next(iter(unit_to_ids))
|
|
197
|
+
cur, ref = await self._agent.worker(reference_unit).get_clock_snapshot()
|
|
198
|
+
wait_count = math.ceil(wait_ms * _CLOCK_FREQUENCY_HZ / 1000)
|
|
199
|
+
target_time = self._trigger_count_proposer.propose_count(cur, ref, wait_count)
|
|
200
|
+
trigger_coros = [
|
|
201
|
+
self._agent.instrument(unit_label).schedule_trigger(self.token, target_time)
|
|
202
|
+
for unit_label in unit_to_ids
|
|
203
|
+
]
|
|
204
|
+
await asyncio.gather(*trigger_coros)
|
|
205
|
+
logger.info(f"trigger scheduled at clock_count={target_time}")
|
|
206
|
+
return target_time
|
{quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/exceptions/__init__.py
RENAMED
|
@@ -72,3 +72,7 @@ class UnitNotFoundError(_ErrorWithUnitLabelsMixin, QuelwareClientError): ...
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
class InvalidUnitStatusError(_ErrorWithUnitLabelsMixin, QuelwareClientError): ...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ServiceUnavailableError(QuelwareClientError):
|
|
78
|
+
DEFAULT_MESSAGE = "The requested service is not available on the server."
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from collections.abc import Collection, Sequence
|
|
2
2
|
from typing import Protocol
|
|
3
3
|
|
|
4
|
-
from quelware_core.entities.clock import CurrentCount, ReferenceCount
|
|
5
4
|
from quelware_core.entities.directives import Directive
|
|
6
5
|
from quelware_core.entities.instrument import InstrumentStatus
|
|
7
6
|
from quelware_core.entities.resource import ResourceId
|
|
@@ -35,10 +34,6 @@ class InstrumentAgent(Protocol):
|
|
|
35
34
|
resource_ids: Collection[ResourceId],
|
|
36
35
|
) -> bool: ...
|
|
37
36
|
|
|
38
|
-
async def get_clock_snapshot(
|
|
39
|
-
self,
|
|
40
|
-
) -> tuple[CurrentCount, ReferenceCount]: ...
|
|
41
|
-
|
|
42
37
|
async def schedule_trigger(
|
|
43
38
|
self,
|
|
44
39
|
token: SessionToken,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Collection
|
|
3
|
+
|
|
4
|
+
from quelware_core.entities.resource import ResourceId
|
|
5
|
+
from quelware_core.entities.session import SessionToken
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TriggerAgent(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
async def trigger(
|
|
11
|
+
self,
|
|
12
|
+
token: SessionToken,
|
|
13
|
+
instrument_ids: Collection[ResourceId],
|
|
14
|
+
requested_min_wait_ms: int | None,
|
|
15
|
+
) -> int: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["TriggerAgent"]
|
|
@@ -3,7 +3,6 @@ from collections.abc import Collection, Sequence
|
|
|
3
3
|
import quelware_core.pb.quelware.instrument.v1 as pb_inst
|
|
4
4
|
from grpclib.client import Channel
|
|
5
5
|
from quelware_core.entities import directives
|
|
6
|
-
from quelware_core.entities.clock import CurrentCount, ReferenceCount
|
|
7
6
|
from quelware_core.entities.instrument import InstrumentStatus
|
|
8
7
|
from quelware_core.entities.resource import ResourceId
|
|
9
8
|
from quelware_core.entities.session import SessionToken
|
|
@@ -79,16 +78,6 @@ class InstrumentAgentGrpc(InstrumentAgent):
|
|
|
79
78
|
await call_with_retry(lambda: self._service.apply(req, metadata=metadata))
|
|
80
79
|
return True
|
|
81
80
|
|
|
82
|
-
@override
|
|
83
|
-
async def get_clock_snapshot(
|
|
84
|
-
self,
|
|
85
|
-
) -> tuple[CurrentCount, ReferenceCount]:
|
|
86
|
-
req = pb_inst.GetClockSnapshotRequest()
|
|
87
|
-
resp = await call_with_retry(
|
|
88
|
-
lambda: self._service.get_clock_snapshot(req), idempotent=True
|
|
89
|
-
)
|
|
90
|
-
return (resp.current_count, resp.reference_count)
|
|
91
|
-
|
|
92
81
|
@override
|
|
93
82
|
async def schedule_trigger(
|
|
94
83
|
self,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
|
|
3
|
+
import quelware_core.pb.quelware.trigger.v1 as pb_trigger
|
|
4
|
+
from betterproto2.grpclib.grpclib_client import MetadataLike
|
|
5
|
+
from grpclib import GRPCError
|
|
6
|
+
from grpclib.client import Channel
|
|
7
|
+
from grpclib.const import Status
|
|
8
|
+
from quelware_core.entities.resource import ResourceId
|
|
9
|
+
from quelware_core.entities.session import SessionToken
|
|
10
|
+
|
|
11
|
+
from quelware_client.core.exceptions import ServiceUnavailableError
|
|
12
|
+
from quelware_client.core.interfaces.trigger_agent import TriggerAgent
|
|
13
|
+
from quelware_client.infra._grpc_retry import call_with_retry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TriggerAgentGrpc(TriggerAgent):
|
|
17
|
+
def __init__(self, grpc_channel: Channel, metadata: MetadataLike | None = None):
|
|
18
|
+
self._channel = grpc_channel
|
|
19
|
+
self._service = pb_trigger.TriggerServiceStub(self._channel, metadata=metadata)
|
|
20
|
+
|
|
21
|
+
async def trigger(
|
|
22
|
+
self,
|
|
23
|
+
token: SessionToken,
|
|
24
|
+
instrument_ids: Collection[ResourceId],
|
|
25
|
+
requested_min_wait_ms: int | None,
|
|
26
|
+
) -> int:
|
|
27
|
+
req = pb_trigger.TriggerRequest(
|
|
28
|
+
instrument_ids=[str(r) for r in instrument_ids],
|
|
29
|
+
requested_min_wait_ms=requested_min_wait_ms,
|
|
30
|
+
)
|
|
31
|
+
metadata = dict(self._service.metadata or {})
|
|
32
|
+
metadata["x-session-token"] = str(token)
|
|
33
|
+
try:
|
|
34
|
+
resp = await call_with_retry(
|
|
35
|
+
lambda: self._service.trigger(req, metadata=metadata)
|
|
36
|
+
)
|
|
37
|
+
except GRPCError as e:
|
|
38
|
+
if e.status is Status.UNIMPLEMENTED:
|
|
39
|
+
raise ServiceUnavailableError(
|
|
40
|
+
f"TriggerService is not available: {e.message}"
|
|
41
|
+
) from e
|
|
42
|
+
# Some gRPC proxies surface upstream missing-route as UNKNOWN + content-type
|
|
43
|
+
# error.
|
|
44
|
+
if e.status is Status.UNKNOWN and e.message and "content-type" in e.message:
|
|
45
|
+
raise ServiceUnavailableError(
|
|
46
|
+
f"TriggerService route not reachable via proxy: {e.message}"
|
|
47
|
+
) from e
|
|
48
|
+
raise
|
|
49
|
+
return resp.scheduled_clock_count
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["TriggerAgentGrpc"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import quelware_core.pb.quelware.worker.v1 as pb_worker
|
|
2
|
+
from grpclib.client import Channel
|
|
3
|
+
from quelware_core.entities.clock import CurrentCount, ReferenceCount
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
from quelware_client.core.interfaces.worker_agent import WorkerAgent
|
|
7
|
+
from quelware_client.infra._grpc_retry import call_with_retry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkerAgentGrpc(WorkerAgent):
|
|
11
|
+
def __init__(self, grpc_channel: Channel, metadata=None):
|
|
12
|
+
self._channel = grpc_channel
|
|
13
|
+
self._service = pb_worker.WorkerServiceStub(self._channel, metadata=metadata)
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
async def get_clock_snapshot(self) -> tuple[CurrentCount, ReferenceCount]:
|
|
17
|
+
req = pb_worker.GetClockSnapshotRequest()
|
|
18
|
+
resp = await call_with_retry(
|
|
19
|
+
lambda: self._service.get_clock_snapshot(req), idempotent=True
|
|
20
|
+
)
|
|
21
|
+
return (resp.current_count, resp.reference_count)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["WorkerAgentGrpc"]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from collections.abc import Collection, Sequence
|
|
2
2
|
|
|
3
|
-
from quelware_core.entities.clock import CurrentCount, ReferenceCount
|
|
4
3
|
from quelware_core.entities.directives import Directive
|
|
5
4
|
from quelware_core.entities.instrument import InstrumentStatus
|
|
6
5
|
from quelware_core.entities.resource import ResourceId
|
|
@@ -41,11 +40,6 @@ class InstrumentAgentMock(InstrumentAgent):
|
|
|
41
40
|
) -> bool:
|
|
42
41
|
return True
|
|
43
42
|
|
|
44
|
-
async def get_clock_snapshot(
|
|
45
|
-
self,
|
|
46
|
-
) -> tuple[CurrentCount, ReferenceCount]:
|
|
47
|
-
return 1234, 1000
|
|
48
|
-
|
|
49
43
|
async def schedule_trigger(
|
|
50
44
|
self,
|
|
51
45
|
token: SessionToken,
|
|
@@ -8,6 +8,8 @@ from quelware_client.testing.session_agent_mock import SessionAgentMock
|
|
|
8
8
|
from quelware_client.testing.system_configuration_agent_mock import (
|
|
9
9
|
SystemConfigurationAgentMock,
|
|
10
10
|
)
|
|
11
|
+
from quelware_client.testing.trigger_agent_mock import TriggerAgentMock
|
|
12
|
+
from quelware_client.testing.worker_agent_mock import WorkerAgentMock
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def create_mock_quelware_client(
|
|
@@ -19,6 +21,7 @@ def create_mock_quelware_client(
|
|
|
19
21
|
agent_container.system_configuration = SystemConfigurationAgentMock(unit_to_status)
|
|
20
22
|
|
|
21
23
|
agent_container.session = SessionAgentMock()
|
|
24
|
+
agent_container.trigger = TriggerAgentMock()
|
|
22
25
|
|
|
23
26
|
def resource_agent_factory(ul: UnitLabel) -> ResourceAgentMock:
|
|
24
27
|
rinfos = unit_to_resources.get(ul, [])
|
|
@@ -27,8 +30,12 @@ def create_mock_quelware_client(
|
|
|
27
30
|
def instrument_agent_factory(ul: UnitLabel) -> InstrumentAgentMock:
|
|
28
31
|
return InstrumentAgentMock()
|
|
29
32
|
|
|
33
|
+
def worker_agent_factory(ul: UnitLabel) -> WorkerAgentMock:
|
|
34
|
+
return WorkerAgentMock()
|
|
35
|
+
|
|
30
36
|
return QuelwareClient(
|
|
31
37
|
agent=agent_container,
|
|
32
38
|
resource_agent_factory=resource_agent_factory,
|
|
33
39
|
instrument_agent_factory=instrument_agent_factory,
|
|
40
|
+
worker_agent_factory=worker_agent_factory,
|
|
34
41
|
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from collections.abc import Collection
|
|
2
|
+
|
|
3
|
+
from quelware_core.entities.resource import ResourceId
|
|
4
|
+
from quelware_core.entities.session import SessionToken
|
|
5
|
+
|
|
6
|
+
from quelware_client.core.interfaces.trigger_agent import TriggerAgent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TriggerAgentMock(TriggerAgent):
|
|
10
|
+
def __init__(self, scheduled_clock_count: int = 1234):
|
|
11
|
+
self._scheduled = scheduled_clock_count
|
|
12
|
+
self.calls: list[tuple[SessionToken, list[ResourceId], int | None]] = []
|
|
13
|
+
|
|
14
|
+
async def trigger(
|
|
15
|
+
self,
|
|
16
|
+
token: SessionToken,
|
|
17
|
+
instrument_ids: Collection[ResourceId],
|
|
18
|
+
requested_min_wait_ms: int | None,
|
|
19
|
+
) -> int:
|
|
20
|
+
self.calls.append((token, list(instrument_ids), requested_min_wait_ms))
|
|
21
|
+
return self._scheduled
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["TriggerAgentMock"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from quelware_core.entities.clock import CurrentCount, ReferenceCount
|
|
2
|
+
|
|
3
|
+
from quelware_client.core.interfaces.worker_agent import WorkerAgent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WorkerAgentMock(WorkerAgent):
|
|
7
|
+
def __init__(self, current_count: int = 1234, reference_count: int = 1000):
|
|
8
|
+
self._current_count = current_count
|
|
9
|
+
self._reference_count = reference_count
|
|
10
|
+
|
|
11
|
+
async def get_clock_snapshot(self) -> tuple[CurrentCount, ReferenceCount]:
|
|
12
|
+
return self._current_count, self._reference_count
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from quelware_core.entities.resource import ResourceId
|
|
3
|
+
from quelware_core.entities.session import SessionToken
|
|
4
|
+
from quelware_core.entities.unit import UnitLabel
|
|
5
|
+
|
|
6
|
+
from quelware_client.core import AgentContainer, Session
|
|
7
|
+
from quelware_client.testing.instrument_agent_mock import InstrumentAgentMock
|
|
8
|
+
from quelware_client.testing.trigger_agent_mock import TriggerAgentMock
|
|
9
|
+
from quelware_client.testing.worker_agent_mock import WorkerAgentMock
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _build_agents(
|
|
13
|
+
units: list[UnitLabel], trigger_agent: TriggerAgentMock
|
|
14
|
+
) -> AgentContainer:
|
|
15
|
+
agents = AgentContainer()
|
|
16
|
+
agents.trigger = trigger_agent
|
|
17
|
+
for u in units:
|
|
18
|
+
agents.update_instrument_agent(u, InstrumentAgentMock())
|
|
19
|
+
agents.update_worker_agent(u, WorkerAgentMock())
|
|
20
|
+
return agents
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_trigger_goes_through_manager_with_requested_wait_ms():
|
|
25
|
+
trig = TriggerAgentMock(scheduled_clock_count=42_000)
|
|
26
|
+
units = [UnitLabel("unit-a"), UnitLabel("unit-b")]
|
|
27
|
+
agents = _build_agents(units, trig)
|
|
28
|
+
rids = [ResourceId(f"{u}:i1") for u in units]
|
|
29
|
+
session = Session(rids, agents, token=SessionToken("tok"))
|
|
30
|
+
|
|
31
|
+
scheduled = await session.trigger(rids, wait_ms=200)
|
|
32
|
+
|
|
33
|
+
assert scheduled == 42_000
|
|
34
|
+
assert len(trig.calls) == 1
|
|
35
|
+
token, sent_ids, wait_ms = trig.calls[0]
|
|
36
|
+
assert token == SessionToken("tok")
|
|
37
|
+
assert set(sent_ids) == set(rids)
|
|
38
|
+
assert wait_ms == 200
|
|
@@ -940,7 +940,7 @@ wheels = [
|
|
|
940
940
|
|
|
941
941
|
[[package]]
|
|
942
942
|
name = "quelware-client"
|
|
943
|
-
version = "0.
|
|
943
|
+
version = "0.3.0"
|
|
944
944
|
source = { editable = "." }
|
|
945
945
|
dependencies = [
|
|
946
946
|
{ name = "betterproto2" },
|
|
@@ -984,7 +984,7 @@ dev = [
|
|
|
984
984
|
|
|
985
985
|
[[package]]
|
|
986
986
|
name = "quelware-core"
|
|
987
|
-
version = "0.
|
|
987
|
+
version = "0.2.0"
|
|
988
988
|
source = { editable = "../quelware-core/python" }
|
|
989
989
|
dependencies = [
|
|
990
990
|
{ name = "betterproto2" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/client/helpers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quelware_client-0.2.0 → quelware_client-0.3.0}/src/quelware_client/core/interfaces/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|