diff --git a/src/blueapi/cli/cli.py b/src/blueapi/cli/cli.py index bd2154f0a..2c150f805 100644 --- a/src/blueapi/cli/cli.py +++ b/src/blueapi/cli/cli.py @@ -337,7 +337,12 @@ def run_plan( instrument_session: str, parameters: TaskParameters, ) -> None: - """Run a plan with parameters""" + """Run a plan with parameters + + To run in the foreground and block until it is complete, stomp + configuration is required. Without stomp configuration, '--bg' can be used + to start a plan in the background. + """ client = cast(BlueapiClient, obj["client"]) task = TaskRequest( diff --git a/tests/unit_tests/cli/test_cli.py b/tests/unit_tests/cli/test_cli.py index 687b3a03f..481acab18 100644 --- a/tests/unit_tests/cli/test_cli.py +++ b/tests/unit_tests/cli/test_cli.py @@ -160,7 +160,7 @@ def test_get_plans(runner: CliRunner): response = responses.add( responses.GET, - "http://localhost:8000/plans", + "http://localhost:8000/api/v1/plans", json=PlanResponse(plans=[PlanModel.from_plan(plan)]).model_dump(), status=200, ) @@ -176,7 +176,7 @@ def test_get_devices(runner: CliRunner): response = responses.add( responses.GET, - "http://localhost:8000/devices", + "http://localhost:8000/api/v1/devices", json=DeviceResponse(devices=[DeviceModel.from_device(device)]).model_dump(), status=200, ) @@ -218,7 +218,7 @@ def test_submit_plan(runner: CliRunner): } response = responses.post( - url="http://a.fake.host:12345/tasks", + url="http://a.fake.host:12345/api/v1/tasks", match=[matchers.json_params_matcher(body_data)], ) @@ -268,7 +268,7 @@ def test_submit_plan_without_stomp(runner: CliRunner): def test_run_plan(stomp_client: StompClient, runner: CliRunner): task_id = "abcd-1234" submit_response = responses.post( - url="http://a.fake.host:12345/tasks", + url="http://a.fake.host:12345/api/v1/tasks", match=[ matchers.json_params_matcher( { @@ -282,7 +282,7 @@ def test_run_plan(stomp_client: StompClient, runner: CliRunner): status=201, ) run_response = responses.put( - url="http://a.fake.host:12345/worker/task", + url="http://a.fake.host:12345/api/v1/worker/task", match=[matchers.json_params_matcher({"task_id": task_id})], json={"task_id": task_id}, ) @@ -398,7 +398,7 @@ def test_run_plan_feedback( @responses.activate def test_run_plan_background_without_stomp(runner: CliRunner): submit_response = responses.post( - url="http://a.fake.host:12345/tasks", + url="http://a.fake.host:12345/api/v1/tasks", match=[ matchers.json_params_matcher( { @@ -412,7 +412,7 @@ def test_run_plan_background_without_stomp(runner: CliRunner): status=201, ) run_response = responses.put( - url="http://a.fake.host:12345/worker/task", + url="http://a.fake.host:12345/api/v1/worker/task", match=[matchers.json_params_matcher({"task_id": "abcd-1234"})], json={"task_id": "abcd-1234"}, ) @@ -541,7 +541,7 @@ def test_get_env(runner: CliRunner): environment_id = uuid.uuid4() responses.add( responses.GET, - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", json=EnvironmentResponse( environment_id=environment_id, initialized=True ).model_dump(mode="json"), @@ -559,7 +559,10 @@ def test_get_env(runner: CliRunner): @responses.activate def test_get_state(runner: CliRunner): responses.add( - responses.GET, "http://localhost:8000/worker/state", json="IDLE", status=200 + responses.GET, + "http://localhost:8000/api/v1/worker/state", + json="IDLE", + status=200, ) state = runner.invoke(main, ["controller", "state"]) print(state.stderr) @@ -576,7 +579,7 @@ def test_reset_env_client_behavior( environment_id = uuid.uuid4() responses.add( responses.DELETE, - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", json=EnvironmentResponse( environment_id=environment_id, initialized=False ).model_dump(mode="json"), @@ -588,7 +591,7 @@ def test_reset_env_client_behavior( for state in env_state: responses.add( responses.GET, - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", json=EnvironmentResponse( environment_id=environment_id, initialized=state ).model_dump(mode="json"), @@ -604,10 +607,10 @@ def test_reset_env_client_behavior( for index, call in enumerate(responses.calls): if index == 0: assert call.request.method == "DELETE" - assert call.request.url == "http://localhost:8000/environment" + assert call.request.url == "http://localhost:8000/api/v1/environment" else: assert call.request.method == "GET" - assert call.request.url == "http://localhost:8000/environment" + assert call.request.url == "http://localhost:8000/api/v1/environment" # Check if the final environment status is printed correctly # assert "Environment is initialized." in result.output @@ -625,7 +628,7 @@ def test_env_timeout(mock_sleep: Mock, runner: CliRunner): environment_id = uuid.uuid4() responses.add( responses.DELETE, - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", status=200, json=EnvironmentResponse( environment_id=environment_id, initialized=False @@ -634,7 +637,7 @@ def test_env_timeout(mock_sleep: Mock, runner: CliRunner): # Add responses for each polling attempt, all indicating not initialized responses.add( responses.GET, - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", json=EnvironmentResponse( environment_id=environment_id, initialized=False ).model_dump(mode="json"), @@ -655,12 +658,12 @@ def test_env_timeout(mock_sleep: Mock, runner: CliRunner): # First call should be DELETE assert responses.calls[0].request.method == "DELETE" - assert responses.calls[0].request.url == "http://localhost:8000/environment" + assert responses.calls[0].request.url == "http://localhost:8000/api/v1/environment" # Remaining calls should all be GET for call in responses.calls[1:]: # Skip the first DELETE request # type: ignore assert call.request.method == "GET" - assert call.request.url == "http://localhost:8000/environment" + assert call.request.url == "http://localhost:8000/api/v1/environment" # Check the output for the timeout message assert result.output == "Reloading environment\n" @@ -673,7 +676,10 @@ def test_env_timeout(mock_sleep: Mock, runner: CliRunner): def test_env_reload_server_side_error(runner: CliRunner): # Setup mocked error response from the server responses.add( - responses.DELETE, "http://localhost:8000/environment", status=500, json={} + responses.DELETE, + "http://localhost:8000/api/v1/environment", + status=500, + json={}, ) result = runner.invoke(main, ["controller", "env", "-r"]) @@ -687,7 +693,7 @@ def test_env_reload_server_side_error(runner: CliRunner): # Only call should be DELETE assert responses.calls[0].request.method == "DELETE" - assert responses.calls[0].request.url == "http://localhost:8000/environment" + assert responses.calls[0].request.url == "http://localhost:8000/api/v1/environment" # Check the output for the timeout message # TODO this seems wrong but this is the current behaviour @@ -1279,7 +1285,7 @@ def test_get_python_environment(runner: CliRunner): } response = responses.add( responses.GET, - "http://localhost:8000/python_environment", + "http://localhost:8000/api/v1/python_environment", json=scratch_config, status=200, ) @@ -1302,7 +1308,7 @@ def test_get_python_env_with_empty_response(runner: CliRunner): } response = responses.add( responses.GET, - "http://localhost:8000/python_environment", + "http://localhost:8000/api/v1/python_environment", json=scratch_config, status=200, ) diff --git a/tests/unit_tests/client/test_rest.py b/tests/unit_tests/client/test_rest.py index 804fb5ee5..e721460ae 100644 --- a/tests/unit_tests/client/test_rest.py +++ b/tests/unit_tests/client/test_rest.py @@ -116,7 +116,7 @@ def test_auth_request_functionality( environment_id = uuid.uuid4() mock_authn_server.stop() # Cannot use multiple RequestsMock context manager mock_get_env = mock_authn_server.get( - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", json=EnvironmentResponse( environment_id=environment_id, initialized=True ).model_dump(mode="json"), @@ -143,7 +143,7 @@ def test_refresh_if_signature_expired( environment_id = uuid.uuid4() mock_authn_server.stop() # Cannot use multiple RequestsMock context manager mock_get_env = mock_authn_server.get( - "http://localhost:8000/environment", + "http://localhost:8000/api/v1/environment", json=EnvironmentResponse( environment_id=environment_id, initialized=True ).model_dump(mode="json"), diff --git a/tests/unit_tests/service/test_rest_api.py b/tests/unit_tests/service/test_rest_api.py index c1d3b6a95..91f7f4089 100644 --- a/tests/unit_tests/service/test_rest_api.py +++ b/tests/unit_tests/service/test_rest_api.py @@ -117,7 +117,7 @@ class MyModel(BaseModel): plan = Plan(name="my-plan", model=MyModel) mock_runner.run.return_value = [PlanModel.from_plan(plan)] - response_get = client_with_cors.get("/plans") + response_get = client_with_cors.get("/api/v1/plans") assert response_get.status_code == status.HTTP_200_OK @@ -135,7 +135,7 @@ def test_rest_config_with_cors( # Allowed method response_post = client_with_cors.post( - "/tasks", + "/api/v1/tasks", json=task.model_dump(), headers={"Accept": "application/json", "Content-Type": "application/json"}, ) @@ -150,7 +150,7 @@ class MyModel(BaseModel): plan = Plan(name="my-plan", model=MyModel) mock_runner.run.return_value = [PlanModel.from_plan(plan)] - response = client.get("/plans") + response = client.get("/api/v1/plans") assert response.status_code == status.HTTP_200_OK assert response.json() == { @@ -176,7 +176,7 @@ class MyModel(BaseModel): plan = Plan(name="my-plan", model=MyModel) mock_runner.run.return_value = PlanModel.from_plan(plan) - response = client.get("/plans/my-plan") + response = client.get("/api/v1/plans/my-plan") mock_runner.run.assert_called_once_with(get_plan, "my-plan") assert response.status_code == status.HTTP_200_OK @@ -194,7 +194,7 @@ class MyModel(BaseModel): def test_get_non_existent_plan_by_name(mock_runner: Mock, client: TestClient) -> None: mock_runner.run.side_effect = KeyError("my-plan") - response = client.get("/plans/my-plan") + response = client.get("/api/v1/plans/my-plan") assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json() == {"detail": "Item not found"} @@ -204,7 +204,7 @@ def test_get_devices(mock_runner: Mock, client: TestClient) -> None: device = MinimalDevice("my-device") mock_runner.run.return_value = [DeviceModel.from_device(device)] - response = client.get("/devices") + response = client.get("/api/v1/devices") assert response.status_code == status.HTTP_200_OK assert response.json() == { @@ -221,7 +221,7 @@ def test_get_device_by_name(mock_runner: Mock, client: TestClient) -> None: device = MinimalDevice("my-device") mock_runner.run.return_value = DeviceModel.from_device(device) - response = client.get("/devices/my-device") + response = client.get("/api/v1/devices/my-device") mock_runner.run.assert_called_once_with(get_device, "my-device") assert response.status_code == status.HTTP_200_OK @@ -233,7 +233,7 @@ def test_get_device_by_name(mock_runner: Mock, client: TestClient) -> None: def test_get_non_existent_device_by_name(mock_runner: Mock, client: TestClient) -> None: mock_runner.run.side_effect = KeyError("my-device") - response = client.get("/devices/my-device") + response = client.get("/api/v1/devices/my-device") assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json() == {"detail": "Item not found"} @@ -249,7 +249,7 @@ def test_create_task(mock_runner: Mock, client: TestClient) -> None: mock_runner.run.side_effect = [task_id] - response = client.post("/tasks", json=task.model_dump()) + response = client.post("/api/v1/tasks", json=task.model_dump()) mock_runner.run.assert_called_with(submit_task, task, {"user": "Unknown"}) assert response.json() == {"task_id": task_id} @@ -270,7 +270,7 @@ def test_create_task_inserts_auth_metadata( # mock_runner.run.side_effect = [task_id] mock_runner.run.return_value = [task_id] - client_with_auth.post("/tasks", json=task.model_dump()) + client_with_auth.post("/api/v1/tasks", json=task.model_dump()) mock_runner.run.assert_called_with(submit_task, task, {"user": "jd1"}) @@ -288,7 +288,7 @@ def test_create_task_validation_error(mock_runner: Mock, client: TestClient) -> ] response = client.post( - "/tasks", + "/api/v1/tasks", json={ "name": "my-plan", "instrument_session": FAKE_INSTRUMENT_SESSION, @@ -310,7 +310,7 @@ def test_create_task_validation_error(mock_runner: Mock, client: TestClient) -> def test_put_plan_begins_task(client: TestClient) -> None: task_id = "04cd9aa6-b902-414b-ae4b-49ea4200e957" - resp = client.put("/worker/task", json={"task_id": task_id}) + resp = client.put("/api/v1/worker/task", json={"task_id": task_id}) assert resp.status_code == status.HTTP_200_OK assert resp.json() == {"task_id": task_id} @@ -325,7 +325,7 @@ def test_put_plan_fails_if_not_idle(mock_runner: Mock, client: TestClient) -> No task=Task(name="none"), task_id=task_id_current, is_complete=False ) - resp = client.put("/worker/task", json={"task_id": task_id_new}) + resp = client.put("/api/v1/worker/task", json={"task_id": task_id_new}) assert resp.status_code == status.HTTP_409_CONFLICT assert resp.json() == {"detail": "Worker already active"} @@ -344,7 +344,7 @@ def test_get_tasks(mock_runner: Mock, client: TestClient) -> None: mock_runner.run.return_value = tasks - response = client.get("/tasks") + response = client.get("/api/v1/tasks") assert response.status_code == status.HTTP_200_OK assert response.json() == { @@ -391,7 +391,7 @@ def test_get_tasks_by_status(mock_runner: Mock, client: TestClient) -> None: mock_runner.run.return_value = tasks - response = client.get("/tasks", params={"task_status": "PENDING"}) + response = client.get("/api/v1/tasks", params={"task_status": "PENDING"}) assert response.json() == { "tasks": [ { @@ -412,14 +412,14 @@ def test_get_tasks_by_status(mock_runner: Mock, client: TestClient) -> None: def test_get_tasks_by_status_invalid(client: TestClient) -> None: - response = client.get("/tasks", params={"task_status": "AN_INVALID_STATUS"}) + response = client.get("/api/v1/tasks", params={"task_status": "AN_INVALID_STATUS"}) assert response.status_code == status.HTTP_400_BAD_REQUEST def test_delete_submitted_task(mock_runner: Mock, client: TestClient) -> None: task_id = str(uuid.uuid4()) mock_runner.run.return_value = task_id - response = client.delete(f"/tasks/{task_id}") + response = client.delete(f"/api/v1/tasks/{task_id}") assert response.json() == {"task_id": f"{task_id}"} @@ -427,7 +427,7 @@ def test_set_active_task(client: TestClient) -> None: task_id = str(uuid.uuid4()) task = WorkerTask(task_id=task_id) - response = client.put("/worker/task", json=task.model_dump()) + response = client.put("/api/v1/worker/task", json=task.model_dump()) assert response.status_code == status.HTTP_200_OK assert response.json() == {"task_id": f"{task_id}"} @@ -446,7 +446,7 @@ def test_set_active_task_active_task_complete( is_pending=False, ) - response = client.put("/worker/task", json=task.model_dump()) + response = client.put("/api/v1/worker/task", json=task.model_dump()) assert response.status_code == status.HTTP_200_OK assert response.json() == {"task_id": f"{task_id}"} @@ -465,7 +465,7 @@ def test_set_active_task_worker_already_running( is_pending=False, ) - response = client.put("/worker/task", json=task.model_dump()) + response = client.put("/api/v1/worker/task", json=task.model_dump()) assert response.status_code == status.HTTP_409_CONFLICT assert response.json() == {"detail": "Worker already active"} @@ -485,7 +485,7 @@ def test_get_task(mock_runner: Mock, client: TestClient): mock_runner.run.return_value = task - response = client.get(f"/tasks/{task_id}") + response = client.get(f"/api/v1/tasks/{task_id}") assert response.json() == { "errors": [], "is_complete": False, @@ -513,7 +513,7 @@ def test_get_all_tasks(mock_runner: Mock, client: TestClient): ] mock_runner.run.return_value = tasks - response = client.get("/tasks") + response = client.get("/api/v1/tasks") assert response.status_code == status.HTTP_200_OK assert response.json() == { "tasks": [ @@ -538,7 +538,7 @@ def test_get_task_error(mock_runner: Mock, client: TestClient): task_id = 567 mock_runner.run.return_value = None - response = client.get(f"/tasks/{task_id}") + response = client.get(f"/api/v1/tasks/{task_id}") assert response.json() == {"detail": "Item not found"} @@ -550,7 +550,7 @@ def test_get_active_task(mock_runner: Mock, client: TestClient): ) mock_runner.run.return_value = task - response = client.get("/worker/task") + response = client.get("/api/v1/worker/task") assert response.json() == {"task_id": f"{task_id}"} @@ -558,7 +558,7 @@ def test_get_active_task(mock_runner: Mock, client: TestClient): def test_get_active_task_none(mock_runner: Mock, client: TestClient): mock_runner.run.return_value = None - response = client.get("/worker/task") + response = client.get("/api/v1/worker/task") assert response.json() == {"task_id": None} @@ -567,7 +567,7 @@ def test_get_state(mock_runner: Mock, client: TestClient): state = WorkerState.SUSPENDING mock_runner.run.return_value = state - response = client.get("/worker/state") + response = client.get("/api/v1/worker/state") assert response.json() == state @@ -577,7 +577,8 @@ def test_set_state_running_to_paused(mock_runner: Mock, client: TestClient): mock_runner.run.side_effect = [current_state, None, final_state] response = client.put( - "/worker/state", json=StateChangeRequest(new_state=final_state).model_dump() + "/api/v1/worker/state", + json=StateChangeRequest(new_state=final_state).model_dump(), ) mock_runner.run.assert_any_call(pause_worker, False) @@ -591,7 +592,8 @@ def test_set_state_paused_to_running(mock_runner: Mock, client: TestClient): mock_runner.run.side_effect = [current_state, None, final_state] response = client.put( - "/worker/state", json=StateChangeRequest(new_state=final_state).model_dump() + "/api/v1/worker/state", + json=StateChangeRequest(new_state=final_state).model_dump(), ) mock_runner.run.assert_any_call(resume_worker) @@ -605,7 +607,8 @@ def test_set_state_running_to_aborting(mock_runner: Mock, client: TestClient): mock_runner.run.side_effect = [current_state, None, final_state] response = client.put( - "/worker/state", json=StateChangeRequest(new_state=final_state).model_dump() + "/api/v1/worker/state", + json=StateChangeRequest(new_state=final_state).model_dump(), ) mock_runner.run.assert_any_call(cancel_active_task, True, None) @@ -622,7 +625,7 @@ def test_set_state_running_to_stopping_including_reason( mock_runner.run.side_effect = [current_state, None, final_state] response = client.put( - "/worker/state", + "/api/v1/worker/state", json=StateChangeRequest(new_state=final_state, reason=reason).model_dump(), ) @@ -638,7 +641,7 @@ def test_set_state_transition_error(mock_runner: Mock, client: TestClient): mock_runner.run.side_effect = [current_state, TransitionError(), final_state] response = client.put( - "/worker/state", + "/api/v1/worker/state", json=StateChangeRequest(new_state=final_state).model_dump(), ) @@ -654,7 +657,7 @@ def test_set_state_invalid_transition(mock_runner: Mock, client: TestClient): mock_runner.run.side_effect = [current_state, final_state] response = client.put( - "/worker/state", + "/api/v1/worker/state", json=StateChangeRequest(new_state=requested_state).model_dump(), ) @@ -670,7 +673,7 @@ def test_get_environment_idle(mock_runner: Mock, client: TestClient) -> None: error_message=None, ) - assert client.get("/environment").json() == { + assert client.get("/api/v1/environment").json() == { "environment_id": str(environment_id), "initialized": True, "error_message": None, @@ -684,7 +687,7 @@ def test_delete_environment(mock_runner: Mock, client: TestClient) -> None: initialized=True, error_message=None, ) - response = client.delete("/environment") + response = client.delete("/api/v1/environment") assert response.status_code is status.HTTP_200_OK assert response.json() == { "environment_id": str(environment_id), @@ -703,7 +706,7 @@ def test_subprocess_enabled_by_default(mp_pool_mock: MagicMock): def test_get_without_authentication(mock_runner: Mock, client: TestClient) -> None: mock_runner.run.side_effect = jwt.PyJWTError - response = client.get("/devices/my-device") + response = client.get("/api/v1/devices/my-device") assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.json() == {"detail": "Not authenticated"} @@ -743,7 +746,7 @@ def test_get_python_environment(mock_runner: Mock, client: TestClient): ] ) mock_runner.run.return_value = packages - response = client.get("/python_environment") + response = client.get("/api/v1/python_environment") assert response.status_code == status.HTTP_200_OK assert response.json() == packages.model_dump() @@ -764,7 +767,7 @@ def test_logout( oidc_config.logout_redirect_endpoint = "/oauth2/sign_out/" mock_runner.run.return_value = oidc_config client_with_auth.follow_redirects = False - response = client_with_auth.get("/logout") + response = client_with_auth.get("/api/v1/logout") assert response.status_code == status.HTTP_308_PERMANENT_REDIRECT assert ( response.headers.get("location") @@ -796,5 +799,5 @@ def test_logout_when_oidc_config_invalid( else: mock_runner.run.return_value = None - response = client_with_auth.get("/logout") + response = client_with_auth.get("/api/v1/logout") assert response.status_code == status.HTTP_205_RESET_CONTENT