Skip to content

Commit 3fde52e

Browse files
committed
Faff around with event buses
1 parent 4c71610 commit 3fde52e

7 files changed

Lines changed: 81 additions & 70 deletions

File tree

docs/reference/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ info:
421421
name: Apache 2.0
422422
url: https://www.apache.org/licenses/LICENSE-2.0.html
423423
title: BlueAPI Control
424-
version: 1.1.3
424+
version: 1.2.0
425425
openapi: 3.1.0
426426
paths:
427427
/config/oidc:

src/blueapi/client/client.py

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import time
22
from concurrent.futures import Future
33

4-
from bluesky_stomp.messaging import MessageContext, StompClient
5-
from bluesky_stomp.models import Broker
6-
from observability_utils.tracing import (
7-
get_tracer,
8-
start_as_current_span,
9-
)
4+
from bluesky_stomp.messaging import MessageContext
5+
from observability_utils.tracing import get_tracer, start_as_current_span
106

117
from blueapi.config import ApplicationConfig, MissingStompConfigurationError
128
from blueapi.core.bluesky_types import DataEvent
@@ -38,15 +34,15 @@ class BlueapiClient:
3834
"""Unified client for controlling blueapi"""
3935

4036
_rest: BlueapiRestClient
41-
_events: EventBusClient | None
37+
_event_bus_client: EventBusClient | None
4238

4339
def __init__(
4440
self,
4541
rest: BlueapiRestClient,
4642
events: EventBusClient | None = None,
4743
):
4844
self._rest = rest
49-
self._events = events
45+
self._event_bus_client = events
5046

5147
@classmethod
5248
def from_config(cls, config: ApplicationConfig) -> "BlueapiClient":
@@ -56,21 +52,8 @@ def from_config(cls, config: ApplicationConfig) -> "BlueapiClient":
5652
except Exception:
5753
... # Swallow exceptions
5854
rest = BlueapiRestClient(config.api, session_manager=session_manager)
59-
stomp_config = config.stomp if config.stomp.enabled else rest.get_stomp_config()
60-
if stomp_config and stomp_config.enabled:
61-
assert stomp_config.url.host is not None, "Stomp URL missing host"
62-
assert stomp_config.url.port is not None, "Stomp URL missing port"
63-
client = StompClient.for_broker(
64-
broker=Broker(
65-
host=stomp_config.url.host,
66-
port=stomp_config.url.port,
67-
auth=stomp_config.auth,
68-
)
69-
)
70-
events = EventBusClient(client)
71-
return cls(rest, events)
72-
else:
73-
return cls(rest)
55+
event_bus = EventBusClient.from_stomp_config(config.stomp)
56+
return cls(rest, event_bus)
7457

7558
@start_as_current_span(TRACER)
7659
def get_plans(self) -> PlanResponse:
@@ -217,7 +200,7 @@ def run_task(
217200
of task execution.
218201
"""
219202

220-
if self._events is None:
203+
if (event_bus := self._event_bus()) is None:
221204
raise MissingStompConfigurationError(
222205
"Stomp configuration required to run plans is missing or disabled"
223206
)
@@ -254,8 +237,8 @@ def inner_on_event(event: AnyEvent, ctx: MessageContext) -> None:
254237
else:
255238
complete.set_result(event)
256239

257-
with self._events:
258-
self._events.subscribe_to_all_events(inner_on_event)
240+
with event_bus:
241+
event_bus.subscribe_to_all_events(inner_on_event)
259242
self.start_task(WorkerTask(task_id=task_id))
260243
return complete.result(timeout=timeout)
261244

@@ -458,3 +441,10 @@ def get_python_env(
458441
"""
459442

460443
return self._rest.get_python_environment(name=name, source=source)
444+
445+
def _event_bus(self) -> EventBusClient | None:
446+
if not self._event_bus_client:
447+
if stomp_config := self._rest.get_stomp_config():
448+
self._event_bus_client = EventBusClient.from_stomp_config(stomp_config)
449+
450+
return self._event_bus_client

src/blueapi/client/event_bus.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from collections.abc import Callable
2+
from typing import Self
23

3-
from bluesky_stomp.messaging import MessageContext, StompClient
4+
from bluesky_stomp.messaging import Broker, MessageContext, StompClient
45
from bluesky_stomp.models import MessageTopic
56

7+
from blueapi.config import StompConfig
68
from blueapi.core import DataEvent
79
from blueapi.worker import ProgressEvent, WorkerEvent
810

@@ -45,3 +47,15 @@ def subscribe_to_all_events(
4547
raise BlueskyStreamingError(
4648
"Unable to subscribe to messages from blueapi"
4749
) from err
50+
51+
@classmethod
52+
def from_stomp_config(cls, config: StompConfig) -> Self | None:
53+
if config.enabled:
54+
assert config.url.host is not None, "Stomp URL missing host"
55+
assert config.url.port is not None, "Stomp URL missing port"
56+
client = StompClient.for_broker(
57+
broker=Broker(
58+
host=config.url.host, port=config.url.port, auth=config.auth
59+
)
60+
)
61+
return cls(client)

src/blueapi/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ class ApplicationConfig(BlueapiBaseModel):
276276
"""
277277

278278
#: API version to publish in OpenAPI schema
279-
REST_API_VERSION: ClassVar[str] = "1.1.3"
279+
REST_API_VERSION: ClassVar[str] = "1.2.0"
280280

281281
LICENSE_INFO: ClassVar[dict[str, str]] = {
282282
"name": "Apache 2.0",

tests/system_tests/test_blueapi_system.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -162,19 +162,21 @@ def expected_devices() -> DeviceResponse:
162162
)
163163

164164

165-
@pytest.fixture
166-
def blueapi_client_get_methods() -> list[str]:
167-
# Get a list of methods that take only one argument (self)
168-
# This will currently return
165+
def authenticated_get_methods() -> list[str]:
166+
# Get a list of methods that take only one argument (self) and require
167+
# authentication. This will currently return
169168
# ['get_plans', 'get_devices', 'get_state', 'get_all_tasks',
170-
# 'get_active_task','get_environment','resume', 'stop','get_oidc_config']
169+
# 'get_active_task','get_environment','resume', 'stop']
171170
return [
172171
method
173172
for method in BlueapiClient.__dict__
174173
if callable(getattr(BlueapiClient, method))
175174
and not method.startswith("__")
176175
and len(inspect.signature(getattr(BlueapiClient, method)).parameters) == 1
177176
and "self" in inspect.signature(getattr(BlueapiClient, method)).parameters
177+
# oidc_config and stomp config can be accessed without auth
178+
and method != "get_oidc_config"
179+
and method != "_event_bus"
178180
]
179181

180182

@@ -212,15 +214,10 @@ def reset_numtracker(server_config: ApplicationConfig):
212214
yield
213215

214216

215-
def test_cannot_access_endpoints(
216-
client_without_auth: BlueapiClient, blueapi_client_get_methods: list[str]
217-
):
218-
blueapi_client_get_methods.remove(
219-
"get_oidc_config"
220-
) # get_oidc_config can be accessed without auth
221-
for get_method in blueapi_client_get_methods:
222-
with pytest.raises(BlueskyRemoteControlError, match=r"<Response \[401\]>"):
223-
getattr(client_without_auth, get_method)()
217+
@pytest.mark.parametrize("method_name", authenticated_get_methods())
218+
def test_cannot_access_endpoints(client_without_auth: BlueapiClient, method_name: str):
219+
with pytest.raises(BlueskyRemoteControlError, match=r"<Response \[401\]>"):
220+
getattr(client_without_auth, method_name)()
224221

225222

226223
def test_can_get_oidc_config_without_auth(client_without_auth: BlueapiClient):

tests/unit_tests/client/test_client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,8 @@ def test_resume(
392392
)
393393

394394

395-
def test_cannot_run_task_without_message_bus(client: BlueapiClient):
395+
def test_cannot_run_task_without_message_bus(client: BlueapiClient, mock_rest: Mock):
396+
mock_rest.get_stomp_config.return_value = None
396397
with pytest.raises(
397398
MissingStompConfigurationError,
398399
match="Stomp configuration required to run plans is missing or disabled",
@@ -660,8 +661,11 @@ def test_resume_span_ok(
660661

661662

662663
def test_cannot_run_task_span_ok(
663-
exporter: JsonObjectSpanExporter, client: BlueapiClient
664+
exporter: JsonObjectSpanExporter,
665+
client: BlueapiClient,
666+
mock_rest: Mock,
664667
):
668+
mock_rest.get_stomp_config.return_value = None
665669
with pytest.raises(
666670
MissingStompConfigurationError,
667671
match="Stomp configuration required to run plans is missing or disabled",

tests/unit_tests/test_cli.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -249,27 +249,30 @@ def test_submit_plan(runner: CliRunner):
249249
@responses.activate
250250
def test_submit_plan_without_stomp(runner: CliRunner):
251251
config_path = "tests/unit_tests/example_yaml/rest_config.yaml"
252-
result = runner.invoke(
253-
main,
254-
[
255-
"-c",
256-
config_path,
257-
"controller",
258-
"run",
259-
"-i",
260-
"cm12345-1",
261-
"sleep",
262-
'{"time": 5}',
263-
],
264-
)
252+
with patch(
253+
"blueapi.client.rest.BlueapiRestClient.get_stomp_config", return_value=None
254+
):
255+
result = runner.invoke(
256+
main,
257+
[
258+
"-c",
259+
config_path,
260+
"controller",
261+
"run",
262+
"-i",
263+
"cm12345-1",
264+
"sleep",
265+
'{"time": 5}',
266+
],
267+
)
265268

266269
assert (
267270
result.stderr
268271
== "Error: Stomp configuration required to run plans is missing or disabled\n"
269272
)
270273

271274

272-
@patch("blueapi.client.client.StompClient")
275+
@patch("blueapi.client.event_bus.StompClient")
273276
@responses.activate
274277
def test_run_plan(stomp_client: StompClient, runner: CliRunner):
275278
task_id = "abcd-1234"
@@ -402,17 +405,20 @@ def test_invalid_stomp_config_for_listener(runner: CliRunner):
402405

403406

404407
def test_cannot_run_plans_without_stomp_config(runner: CliRunner):
405-
result = runner.invoke(
406-
main,
407-
[
408-
"controller",
409-
"run",
410-
"-i",
411-
"cm12345-1",
412-
"sleep",
413-
'{"time": 5}',
414-
],
415-
)
408+
with patch(
409+
"blueapi.client.rest.BlueapiRestClient.get_stomp_config", return_value=None
410+
):
411+
result = runner.invoke(
412+
main,
413+
[
414+
"controller",
415+
"run",
416+
"-i",
417+
"cm12345-1",
418+
"sleep",
419+
'{"time": 5}',
420+
],
421+
)
416422
assert result.exit_code == 1
417423
assert (
418424
result.stderr

0 commit comments

Comments
 (0)