From fddfe69e7b94ce406c29595a393652547c7bc999 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 24 Apr 2026 10:58:51 +0000 Subject: [PATCH 1/2] Add versioned secure router --- src/blueapi/service/main.py | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 81f9f849ee..679119238b 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -99,8 +99,9 @@ async def inner(app: FastAPI): return inner -secure_router = APIRouter() open_router = APIRouter() +secure_router = APIRouter(deprecated=True) +secure_router_v1 = APIRouter(prefix="/api/v1") def get_app(config: ApplicationConfig): @@ -122,6 +123,7 @@ def get_app(config: ApplicationConfig): } app.include_router(open_router) app.include_router(secure_router, dependencies=dependencies) + app.include_router(secure_router_v1, 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_version_headers) @@ -188,6 +190,7 @@ def root_redirect() -> RedirectResponse: ) +@secure_router_v1.get("/environment", tags=[Tag.ENV]) @secure_router.get("/environment", tags=[Tag.ENV]) @start_as_current_span(TRACER, "runner") def get_environment( @@ -197,6 +200,7 @@ def get_environment( return runner.state +@secure_router_v1.delete("/environment", tags=[Tag.ENV]) @secure_router.delete("/environment", tags=[Tag.ENV]) async def delete_environment( background_tasks: BackgroundTasks, @@ -227,6 +231,7 @@ def get_oidc_config( return config +@secure_router_v1.get("/plans", tags=[Tag.PLAN]) @secure_router.get("/plans", tags=[Tag.PLAN]) @start_as_current_span(TRACER) def get_plans(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> PlanResponse: @@ -235,6 +240,7 @@ def get_plans(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> PlanResp return PlanResponse(plans=plans) +@secure_router_v1.get("/plans/{name}", tags=[Tag.PLAN]) @secure_router.get("/plans/{name}", tags=[Tag.PLAN]) @start_as_current_span(TRACER, "name") def get_plan_by_name( @@ -244,6 +250,7 @@ def get_plan_by_name( return runner.run(interface.get_plan, name) +@secure_router_v1.get("/devices", tags=[Tag.DEVICE]) @secure_router.get("/devices", tags=[Tag.DEVICE]) @start_as_current_span(TRACER) def get_devices( @@ -254,6 +261,7 @@ def get_devices( return DeviceResponse(devices=devices) +@secure_router_v1.get("/devices/{name}", tags=[Tag.DEVICE]) @secure_router.get("/devices/{name}", tags=[Tag.DEVICE]) @start_as_current_span(TRACER, "name") def get_device_by_name( @@ -270,6 +278,7 @@ def get_device_by_name( ) +@secure_router_v1.post("/tasks", status_code=status.HTTP_201_CREATED, tags=[Tag.TASK]) @secure_router.post("/tasks", status_code=status.HTTP_201_CREATED, tags=[Tag.TASK]) @start_as_current_span( TRACER, @@ -318,6 +327,9 @@ def submit_task( ) from e +@secure_router_v1.delete( + "/tasks/{task_id}", status_code=status.HTTP_200_OK, tags=[Tag.TASK] +) @secure_router.delete( "/tasks/{task_id}", status_code=status.HTTP_200_OK, tags=[Tag.TASK] ) @@ -337,6 +349,7 @@ def validate_task_status(v: str) -> TaskStatusEnum: return TaskStatusEnum(v_upper) +@secure_router_v1.get("/tasks", status_code=status.HTTP_200_OK, tags=[Tag.TASK]) @secure_router.get("/tasks", status_code=status.HTTP_200_OK, tags=[Tag.TASK]) @start_as_current_span(TRACER) def get_tasks( @@ -363,6 +376,11 @@ def get_tasks( return TasksListResponse(tasks=tasks) +@secure_router_v1.put( + "/worker/task", + responses={status.HTTP_409_CONFLICT: {}}, + tags=[Tag.TASK], +) @secure_router.put( "/worker/task", responses={status.HTTP_409_CONFLICT: {}}, @@ -397,6 +415,7 @@ def get_passthrough_headers(request: Request) -> dict[str, str]: } +@secure_router_v1.get("/tasks/{task_id}", tags=[Tag.TASK]) @secure_router.get("/tasks/{task_id}", tags=[Tag.TASK]) @start_as_current_span(TRACER, "task_id") def get_task( @@ -410,6 +429,10 @@ def get_task( return task +@secure_router_v1.get( + "/worker/task", + tags=[Tag.TASK], +) @secure_router.get( "/worker/task", tags=[Tag.TASK], @@ -423,6 +446,10 @@ def get_active_task( return WorkerTask(task_id=task_id) +@secure_router_v1.get( + "/worker/state", + tags=[Tag.TASK], +) @secure_router.get( "/worker/state", tags=[Tag.TASK], @@ -448,6 +475,15 @@ def get_state(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> WorkerSt } +@secure_router_v1.put( + "/worker/state", + status_code=status.HTTP_202_ACCEPTED, + responses={ + status.HTTP_400_BAD_REQUEST: {}, + status.HTTP_202_ACCEPTED: {}, + }, + tags=[Tag.TASK], +) @secure_router.put( "/worker/state", status_code=status.HTTP_202_ACCEPTED, @@ -506,6 +542,7 @@ def set_state( return runner.run(interface.get_worker_state) +@secure_router_v1.get("/python_environment", tags=[Tag.ENV]) @secure_router.get("/python_environment", tags=[Tag.ENV]) @start_as_current_span(TRACER) def get_python_environment( @@ -527,6 +564,7 @@ def health_probe() -> HealthProbeResponse: return HealthProbeResponse(status=Health.OK) +@secure_router_v1.get("/logout", include_in_schema=False) @secure_router.get("/logout", include_in_schema=False) def logout(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: """Redirect to logout url""" From 35fd309459886620060f0006c498589ac0e22da4 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 24 Apr 2026 10:59:12 +0000 Subject: [PATCH 2/2] Update open api schema --- docs/reference/openapi.yaml | 377 +++++++++++++++++++++++++++++++++++- src/blueapi/config.py | 2 +- 2 files changed, 377 insertions(+), 2 deletions(-) diff --git a/docs/reference/openapi.yaml b/docs/reference/openapi.yaml index ac8b9401b7..d14664f571 100644 --- a/docs/reference/openapi.yaml +++ b/docs/reference/openapi.yaml @@ -433,9 +433,369 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html title: BlueAPI Control - version: 1.2.0 + version: 1.3.0 openapi: 3.1.0 paths: + /api/v1/devices: + get: + description: Retrieve information about all available devices. + operationId: get_devices_api_v1_devices_get + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceResponse' + description: Successful Response + summary: Get Devices + tags: + - Device + /api/v1/devices/{name}: + get: + description: Retrieve information about a devices by its (unique) name. + operationId: get_device_by_name_api_v1_devices__name__get + parameters: + - in: path + name: name + required: true + schema: + title: Name + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceModel' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Get Device By Name + tags: + - Device + /api/v1/environment: + delete: + description: Delete the current environment, causing internal components to + be reloaded. + operationId: delete_environment_api_v1_environment_delete + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentResponse' + description: Successful Response + summary: Delete Environment + tags: + - Environment + get: + description: Get the current state of the environment, i.e. initialization state. + operationId: get_environment_api_v1_environment_get + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentResponse' + description: Successful Response + summary: Get Environment + tags: + - Environment + /api/v1/plans: + get: + description: Retrieve information about all available plans. + operationId: get_plans_api_v1_plans_get + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PlanResponse' + description: Successful Response + summary: Get Plans + tags: + - Plan + /api/v1/plans/{name}: + get: + description: Retrieve information about a plan by its (unique) name. + operationId: get_plan_by_name_api_v1_plans__name__get + parameters: + - in: path + name: name + required: true + schema: + title: Name + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PlanModel' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Get Plan By Name + tags: + - Plan + /api/v1/python_environment: + get: + description: 'Retrieve the Python environment details. + + This endpoint fetches information about the Python environment, + + such as the installed packages and scratch packages.' + operationId: get_python_environment_api_v1_python_environment_get + parameters: + - in: query + name: name + required: false + schema: + anyOf: + - type: string + - type: 'null' + title: Name + - in: query + name: source + required: false + schema: + anyOf: + - $ref: '#/components/schemas/SourceInfo' + - type: 'null' + title: Source + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PythonEnvironmentResponse' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Get Python Environment + tags: + - Environment + /api/v1/tasks: + get: + description: 'Retrieve tasks based on their status. + + The status of a newly created task is ''unstarted''.' + operationId: get_tasks_api_v1_tasks_get + parameters: + - in: query + name: task_status + required: false + schema: + title: Task Status + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TasksListResponse' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Get Tasks + tags: + - Task + post: + description: Submit a task to the worker. + operationId: submit_task_api_v1_tasks_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TaskRequest' + examples: + - instrument_session: cm12345-1 + name: count + params: + detectors: + - x + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/TaskResponse' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Submit Task + tags: + - Task + /api/v1/tasks/{task_id}: + delete: + operationId: delete_submitted_task_api_v1_tasks__task_id__delete + parameters: + - in: path + name: task_id + required: true + schema: + title: Task Id + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TaskResponse' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Delete Submitted Task + tags: + - Task + get: + description: Retrieve a task + operationId: get_task_api_v1_tasks__task_id__get + parameters: + - in: path + name: task_id + required: true + schema: + title: Task Id + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TrackableTask' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Get Task + tags: + - Task + /api/v1/worker/state: + get: + description: Get the State of the Worker + operationId: get_state_api_v1_worker_state_get + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/WorkerState' + description: Successful Response + summary: Get State + tags: + - Task + put: + description: "Request that the worker is put into a particular state.\nReturns\ + \ the state of the worker at the end of the call.\n\n- **The following transitions\ + \ are allowed and return 202: Accepted**\n- If the worker is **PAUSED**, new_state\ + \ may be **RUNNING** to resume.\n- If the worker is **RUNNING**, new_state\ + \ may be **PAUSED** to pause:\n - If defer is False (default): pauses and\ + \ rewinds to the previous checkpoint\n - If defer is True: waits until\ + \ the next checkpoint to pause\n - **If the task has no checkpoints, the\ + \ task will instead be Aborted**\n- If the worker is **RUNNING/PAUSED**, new_state\ + \ may be **STOPPING** to stop.\n Stop marks any currently open Runs in\ + \ the Task as a success and ends the task.\n- If the worker is **RUNNING/PAUSED**,\ + \ new_state may be **ABORTING** to abort.\n Abort marks any currently open\ + \ Runs in the Task as a Failure and ends the task.\n - If reason is set,\ + \ the reason will be passed as the reason for the Run failure.\n- **All other\ + \ transitions return 400: Bad Request**" + operationId: set_state_api_v1_worker_state_put + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/StateChangeRequest' + required: true + responses: + '202': + content: + application/json: + schema: + $ref: '#/components/schemas/WorkerState' + description: Successful Response + '400': + description: Bad Request + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Set State + tags: + - Task + /api/v1/worker/task: + get: + operationId: get_active_task_api_v1_worker_task_get + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/WorkerTask' + description: Successful Response + summary: Get Active Task + tags: + - Task + put: + description: 'Set a task to active status, the worker should begin it as soon + as possible. + + This will return an error response if the worker is not idle.' + operationId: set_active_task_api_v1_worker_task_put + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/WorkerTask' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/WorkerTask' + description: Successful Response + '409': + description: Conflict + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Set Active Task + tags: + - Task /config/oidc: get: description: Retrieve the OpenID Connect (OIDC) configuration for the server. @@ -454,6 +814,7 @@ paths: - Meta /devices: get: + deprecated: true description: Retrieve information about all available devices. operationId: get_devices_devices_get responses: @@ -468,6 +829,7 @@ paths: - Device /devices/{name}: get: + deprecated: true description: Retrieve information about a devices by its (unique) name. operationId: get_device_by_name_devices__name__get parameters: @@ -495,6 +857,7 @@ paths: - Device /environment: delete: + deprecated: true description: Delete the current environment, causing internal components to be reloaded. operationId: delete_environment_environment_delete @@ -509,6 +872,7 @@ paths: tags: - Environment get: + deprecated: true description: Get the current state of the environment, i.e. initialization state. operationId: get_environment_environment_get responses: @@ -537,6 +901,7 @@ paths: - Meta /plans: get: + deprecated: true description: Retrieve information about all available plans. operationId: get_plans_plans_get responses: @@ -551,6 +916,7 @@ paths: - Plan /plans/{name}: get: + deprecated: true description: Retrieve information about a plan by its (unique) name. operationId: get_plan_by_name_plans__name__get parameters: @@ -578,6 +944,7 @@ paths: - Plan /python_environment: get: + deprecated: true description: 'Retrieve the Python environment details. This endpoint fetches information about the Python environment, @@ -619,6 +986,7 @@ paths: - Environment /tasks: get: + deprecated: true description: 'Retrieve tasks based on their status. The status of a newly created task is ''unstarted''.' @@ -647,6 +1015,7 @@ paths: tags: - Task post: + deprecated: true description: Submit a task to the worker. operationId: submit_task_tasks_post requestBody: @@ -679,6 +1048,7 @@ paths: - Task /tasks/{task_id}: delete: + deprecated: true operationId: delete_submitted_task_tasks__task_id__delete parameters: - in: path @@ -704,6 +1074,7 @@ paths: tags: - Task get: + deprecated: true description: Retrieve a task operationId: get_task_tasks__task_id__get parameters: @@ -731,6 +1102,7 @@ paths: - Task /worker/state: get: + deprecated: true description: Get the State of the Worker operationId: get_state_worker_state_get responses: @@ -744,6 +1116,7 @@ paths: tags: - Task put: + deprecated: true description: "Request that the worker is put into a particular state.\nReturns\ \ the state of the worker at the end of the call.\n\n- **The following transitions\ \ are allowed and return 202: Accepted**\n- If the worker is **PAUSED**, new_state\ @@ -785,6 +1158,7 @@ paths: - Task /worker/task: get: + deprecated: true operationId: get_active_task_worker_task_get responses: '200': @@ -797,6 +1171,7 @@ paths: tags: - Task put: + deprecated: true description: 'Set a task to active status, the worker should begin it as soon as possible. diff --git a/src/blueapi/config.py b/src/blueapi/config.py index 2adbbfd047..83d6d70211 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -303,7 +303,7 @@ class ApplicationConfig(BlueapiBaseModel): """ #: API version to publish in OpenAPI schema - REST_API_VERSION: ClassVar[str] = "1.2.0" + REST_API_VERSION: ClassVar[str] = "1.3.0" LICENSE_INFO: ClassVar[dict[str, str]] = { "name": "Apache 2.0",