From 04c0062c5539b9f533da4f846ee8912eab5f5137 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 19 Dec 2025 16:45:48 +0000 Subject: [PATCH] wip --- src/blueapi/cli/cli.py | 33 ++++++++++++++++++++++++++++++-- src/blueapi/client/client.py | 3 +++ src/blueapi/client/rest.py | 5 ++++- src/blueapi/service/interface.py | 4 ++++ src/blueapi/service/main.py | 18 ++++++++++++++++- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/blueapi/cli/cli.py b/src/blueapi/cli/cli.py index c39ff13dff..e7cb43402d 100644 --- a/src/blueapi/cli/cli.py +++ b/src/blueapi/cli/cli.py @@ -16,7 +16,7 @@ from bluesky_stomp.models import Broker from click.exceptions import ClickException from observability_utils.tracing import setup_tracing -from pydantic import ValidationError +from pydantic import HttpUrl, ValidationError from requests.exceptions import ConnectionError from blueapi import __version__, config @@ -32,6 +32,7 @@ from blueapi.config import ( ApplicationConfig, ConfigLoader, + StompConfig, ) from blueapi.core import OTLP_EXPORT_ENABLED, DataEvent from blueapi.log import set_up_logging @@ -159,8 +160,18 @@ def start_application(obj: dict): type=click.Choice([o.name.lower() for o in OutputFormat]), default="compact", ) +@click.option( + "--url", + type=str, + help="URL for the blueapi server to connect to.", + default=None, +) @click.pass_context -def controller(ctx: click.Context, output: str) -> None: +def controller( + ctx: click.Context, + output: str, + url: str | None, +) -> None: """Client utility for controlling and introspecting the worker""" setup_tracing("BlueAPICLI", OTLP_EXPORT_ENABLED) @@ -170,9 +181,18 @@ def controller(ctx: click.Context, output: str) -> None: ctx.ensure_object(dict) config: ApplicationConfig = ctx.obj["config"] + + if url is not None: + config.api.url = HttpUrl(url) + ctx.obj["fmt"] = OutputFormat(output) ctx.obj["client"] = BlueapiClient.from_config(config) + client: BlueapiClient = ctx.obj["client"] + stomp_config: StompConfig = client.get_stomp_config() + config.stomp = stomp_config + ctx.obj["client"] = BlueapiClient.from_config(config) + P = ParamSpec("P") T = TypeVar("T") @@ -198,6 +218,15 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: return wrapper +@controller.command(name="stomp") +@click.pass_obj +@check_connection +def get_stomp_config(obj: dict) -> None: + """Get the stomp config the server is using""" + client: BlueapiClient = obj["client"] + obj["fmt"].display(client.get_stomp_config()) + + @controller.command(name="plans") @click.pass_obj @check_connection diff --git a/src/blueapi/client/client.py b/src/blueapi/client/client.py index 0930e240a9..3a896c8e6d 100644 --- a/src/blueapi/client/client.py +++ b/src/blueapi/client/client.py @@ -71,6 +71,9 @@ def from_config(cls, config: ApplicationConfig) -> "BlueapiClient": else: return cls(rest) + def get_stomp_config(self): + return self._rest.get_stomp_config() + @start_as_current_span(TRACER) def get_plans(self) -> PlanResponse: """ diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index 3ff119449e..33676aa4ad 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -10,7 +10,7 @@ ) from pydantic import BaseModel, TypeAdapter, ValidationError -from blueapi.config import RestConfig +from blueapi.config import RestConfig, StompConfig from blueapi.service.authentication import JWTAuth, SessionManager from blueapi.service.model import ( DeviceModel, @@ -215,6 +215,9 @@ def cancel_current_task( data={"new_state": state, "reason": reason}, ) + def get_stomp_config(self): + return self._request_and_deserialize("/config/stomp", StompConfig) + def get_environment(self) -> EnvironmentResponse: return self._request_and_deserialize("/environment", EnvironmentResponse) diff --git a/src/blueapi/service/interface.py b/src/blueapi/service/interface.py index 74a95291cc..7dd24581e0 100644 --- a/src/blueapi/service/interface.py +++ b/src/blueapi/service/interface.py @@ -251,6 +251,10 @@ def get_oidc_config() -> OIDCConfig | None: return config().oidc +def get_stomp_config() -> StompConfig | None: + return config().stomp + + def get_python_env( name: str | None = None, source: SourceInfo | None = None ) -> PythonEnvironmentResponse: diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index ba5d022d2c..e8f6596927 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -34,7 +34,7 @@ from starlette.responses import JSONResponse from super_state_machine.errors import TransitionError -from blueapi.config import ApplicationConfig, OIDCConfig +from blueapi.config import ApplicationConfig, OIDCConfig, StompConfig from blueapi.service import interface from blueapi.worker import TrackableTask, WorkerState from blueapi.worker.event import TaskStatusEnum @@ -244,6 +244,22 @@ def get_oidc_config( return config +@open_router.get( + "/config/stomp", + tags=[Tag.META], + responses={status.HTTP_204_NO_CONTENT: {"description": "No Stomp configured"}}, +) +@start_as_current_span(TRACER) +def get_stomp_config( + runner: Annotated[WorkerDispatcher, Depends(_runner)], +) -> StompConfig: + """Retrieve the stomp configuration for the server.""" + config = runner.run(interface.get_stomp_config) + if config is None: + raise HTTPException(status_code=status.HTTP_204_NO_CONTENT) + return config + + @secure_router.get("/plans", tags=[Tag.PLAN]) @start_as_current_span(TRACER) def get_plans(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> PlanResponse: