From 6072ba70b1d8af80bc3f7af6bec0e9a803b8dfef Mon Sep 17 00:00:00 2001 From: Shreelakshmi Iyengar Date: Thu, 12 Mar 2026 11:59:47 +0000 Subject: [PATCH 1/7] wip: add blueapi version number to header --- src/blueapi/service/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index c79dd3df34..21be3649c1 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -34,6 +34,8 @@ from starlette.responses import JSONResponse from super_state_machine.errors import TransitionError +import blueapi +import blueapi.cli from blueapi.config import ApplicationConfig, OIDCConfig, Tag from blueapi.service import interface from blueapi.worker import TrackableTask, WorkerState @@ -573,6 +575,7 @@ async def add_api_version_header( ): response = await call_next(request) response.headers["X-API-Version"] = ApplicationConfig.REST_API_VERSION + response.headers["X-BlueAPI-Version"] = blueapi.__version__ return response From 70eb88e4a60a2f1be7e42c1d61e641ee088b422b Mon Sep 17 00:00:00 2001 From: Shreelakshmi Iyengar Date: Fri, 13 Mar 2026 12:01:16 +0000 Subject: [PATCH 2/7] tests: add test for adding version headers to reponse --- src/blueapi/service/main.py | 4 ++-- tests/unit_tests/service/test_main.py | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 21be3649c1..50332374d8 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -125,7 +125,7 @@ def get_app(config: ApplicationConfig): app.include_router(secure_router, dependencies=dependencies) app.add_exception_handler(KeyError, on_key_error_404) app.add_exception_handler(jwt.PyJWTError, on_token_error_401) - app.middleware("http")(add_api_version_header) + app.middleware("http")(add_version_headers) app.middleware("http")(inject_propagated_observability_context) app.middleware("http")(log_request_details) if config.api.cors: @@ -570,7 +570,7 @@ def start(config: ApplicationConfig): ) -async def add_api_version_header( +async def add_version_headers( request: Request, call_next: Callable[[Request], Awaitable[Response]] ): response = await call_next(request) diff --git a/tests/unit_tests/service/test_main.py b/tests/unit_tests/service/test_main.py index 2e109d38c6..129bdd2898 100644 --- a/tests/unit_tests/service/test_main.py +++ b/tests/unit_tests/service/test_main.py @@ -5,7 +5,28 @@ from fastapi import FastAPI, Request from fastapi.testclient import TestClient -from blueapi.service.main import get_passthrough_headers, log_request_details +import blueapi +from blueapi.config import ApplicationConfig +from blueapi.service.main import ( + add_version_headers, + get_passthrough_headers, + log_request_details, +) + + +async def test_add_version_header(): + app = FastAPI() + app.middleware("http")(add_version_headers) + + @app.get("/") + async def root(): + return {"message": "Hello World"} + + client = TestClient(app) + response = client.get("/") + + assert response.headers["X-API-VERSION"] == ApplicationConfig.REST_API_VERSION + assert response.headers["X-BlueAPI-VERSION"] == blueapi.__version__ async def test_log_request_details(): From 396b3b697844b347e5797aa46064926c82434c37 Mon Sep 17 00:00:00 2001 From: Shreelakshmi Iyengar Date: Mon, 16 Mar 2026 13:28:42 +0000 Subject: [PATCH 3/7] wip: add check of version numbers --- src/blueapi/client/rest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index 52150d36fd..1de25d2bb5 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -271,6 +271,10 @@ def _request_and_deserialize( raise exception if response.status_code == status.HTTP_204_NO_CONTENT: raise NoContentError(target_type) + # if response.headers.get("version") != our_version: + + # if response.headers.get("version") + # do something deserialized = TypeAdapter(target_type).validate_python(response.json()) return deserialized From d8e4997c7e1b7e372238dedffc11b1b9e9319a07 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:52:08 +0000 Subject: [PATCH 4/7] add check for server version --- src/blueapi/client/rest.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index 1de25d2bb5..6ff3cf23aa 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -1,3 +1,4 @@ +import logging from collections.abc import Callable, Mapping from typing import Any, Literal, TypeVar @@ -10,6 +11,7 @@ ) from pydantic import BaseModel, TypeAdapter, ValidationError +from blueapi import __version__ from blueapi.config import RestConfig from blueapi.service.authentication import JWTAuth, SessionManager from blueapi.service.model import ( @@ -271,10 +273,17 @@ def _request_and_deserialize( raise exception if response.status_code == status.HTTP_204_NO_CONTENT: raise NoContentError(target_type) - # if response.headers.get("version") != our_version: - - # if response.headers.get("version") - # do something + server_version = response.headers.get("x-blueapi-version") + if server_version is None: + logging.warning("Cannot get find server version") + else: + from packaging.version import Version + + if Version(server_version).release != Version(__version__).release: + logging.warning( + f"Server version is {Version(server_version).release} and " + "client version is {Version(__version__).release}" + ) deserialized = TypeAdapter(target_type).validate_python(response.json()) return deserialized From 704e610531cb36f5b96545c66d599167043b13c4 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:01:58 +0000 Subject: [PATCH 5/7] fix minor things --- src/blueapi/client/rest.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index 6ff3cf23aa..98eaa78ca8 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -273,16 +273,13 @@ def _request_and_deserialize( raise exception if response.status_code == status.HTTP_204_NO_CONTENT: raise NoContentError(target_type) - server_version = response.headers.get("x-blueapi-version") - if server_version is None: - logging.warning("Cannot get find server version") - else: + if (server_version := response.headers.get("x-blueapi-version")) is not None: from packaging.version import Version - if Version(server_version).release != Version(__version__).release: + if Version(server_version).release == Version(__version__).release: logging.warning( f"Server version is {Version(server_version).release} and " - "client version is {Version(__version__).release}" + f"client version is {Version(__version__).release}" ) deserialized = TypeAdapter(target_type).validate_python(response.json()) return deserialized From ee9cf9ec5781d4f1cbda1ded6f4f135fbc1431c7 Mon Sep 17 00:00:00 2001 From: Shreelakshmi Iyengar Date: Tue, 17 Mar 2026 16:55:55 +0000 Subject: [PATCH 6/7] test: add test to check server version comparison in header of response --- src/blueapi/client/rest.py | 6 +++-- tests/unit_tests/client/test_rest.py | 35 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index 98eaa78ca8..fc7e5c1b42 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -34,6 +34,8 @@ TRACER = get_tracer("rest") +LOGGER = logging.getLogger(__name__) + class UnauthorisedAccessError(Exception): pass @@ -276,8 +278,8 @@ def _request_and_deserialize( if (server_version := response.headers.get("x-blueapi-version")) is not None: from packaging.version import Version - if Version(server_version).release == Version(__version__).release: - logging.warning( + if Version(server_version).release != Version(__version__).release: + LOGGER.warning( f"Server version is {Version(server_version).release} and " f"client version is {Version(__version__).release}" ) diff --git a/tests/unit_tests/client/test_rest.py b/tests/unit_tests/client/test_rest.py index 2ddcdd3800..6c574f56f6 100644 --- a/tests/unit_tests/client/test_rest.py +++ b/tests/unit_tests/client/test_rest.py @@ -1,11 +1,13 @@ import uuid from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest import requests import responses +from packaging.version import Version +from blueapi import __version__ from blueapi.client.rest import ( BlueapiRestClient, BlueskyRemoteControlError, @@ -196,3 +198,34 @@ def test_parameter_error_other_string(): input=34, ) assert str(p1) == "Invalid value 34 for field field_one.0: error_message" + + +@pytest.mark.parametrize( + "server_version,logging_warning_present", + [(__version__, False), ("0.0.1", True), (None, False)], +) +@patch("blueapi.client.rest.TypeAdapter") +@patch("blueapi.client.rest.requests.Session.request") +@patch("blueapi.client.rest.LOGGER") +def test_server_and_client_versions( + mock_logger: MagicMock, + mock_request: Mock, + mock_type_adapter: Mock, + rest: BlueapiRestClient, + server_version: str, + logging_warning_present: bool, +): + response = Mock(spec=requests.Response) + response.status_code = 200 + response.headers = {"x-blueapi-version": server_version} + mock_request.return_value = response + + rest.get_plans() + + if logging_warning_present: + mock_logger.warning.assert_called_once_with( + f"Server version is {Version(server_version).release} and " + f"client version is {Version(__version__).release}" + ) + else: + mock_logger.assert_not_called() From 12a3e0a403c8eb57f3a62ff58f93373f756c7e3a Mon Sep 17 00:00:00 2001 From: Shreelakshmi Iyengar Date: Wed, 18 Mar 2026 16:18:04 +0000 Subject: [PATCH 7/7] wip: change logger warning message based on PR comments --- src/blueapi/client/rest.py | 9 ++++++--- tests/unit_tests/client/test_rest.py | 6 ++++-- tests/unit_tests/service/test_main.py | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/blueapi/client/rest.py b/src/blueapi/client/rest.py index fc7e5c1b42..4e5db254ae 100644 --- a/src/blueapi/client/rest.py +++ b/src/blueapi/client/rest.py @@ -278,10 +278,13 @@ def _request_and_deserialize( if (server_version := response.headers.get("x-blueapi-version")) is not None: from packaging.version import Version - if Version(server_version).release != Version(__version__).release: + if (server_version := Version(server_version).release) != ( + client_version := Version(__version__).release + ): LOGGER.warning( - f"Server version is {Version(server_version).release} and " - f"client version is {Version(__version__).release}" + f"Version mismatch: Blueapi server version is {server_version}" + f"but client version is {client_version}." + f"Some features may not work as expected." ) deserialized = TypeAdapter(target_type).validate_python(response.json()) return deserialized diff --git a/tests/unit_tests/client/test_rest.py b/tests/unit_tests/client/test_rest.py index 6c574f56f6..e34ee4f8ca 100644 --- a/tests/unit_tests/client/test_rest.py +++ b/tests/unit_tests/client/test_rest.py @@ -224,8 +224,10 @@ def test_server_and_client_versions( if logging_warning_present: mock_logger.warning.assert_called_once_with( - f"Server version is {Version(server_version).release} and " - f"client version is {Version(__version__).release}" + f"Version mismatch: Blueapi server version is" + f"{Version(server_version).release}" + f"but client version is {Version(__version__).release}." + f"Some features may not work as expected." ) else: mock_logger.assert_not_called() diff --git a/tests/unit_tests/service/test_main.py b/tests/unit_tests/service/test_main.py index 129bdd2898..4a4bcca634 100644 --- a/tests/unit_tests/service/test_main.py +++ b/tests/unit_tests/service/test_main.py @@ -5,7 +5,7 @@ from fastapi import FastAPI, Request from fastapi.testclient import TestClient -import blueapi +from blueapi import __version__ from blueapi.config import ApplicationConfig from blueapi.service.main import ( add_version_headers, @@ -26,7 +26,7 @@ async def root(): response = client.get("/") assert response.headers["X-API-VERSION"] == ApplicationConfig.REST_API_VERSION - assert response.headers["X-BlueAPI-VERSION"] == blueapi.__version__ + assert response.headers["X-BlueAPI-VERSION"] == __version__ async def test_log_request_details():