Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardise api #222

Merged
merged 8 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions catalog-info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ metadata:
name: blueapiControl
title: Athena BlueAPI Control
description: REST API for getting plans/devices from the worker (and running tasks)
annotations:
diamond.ac.uk/viewdocs-url: https://diamondlightsource.github.io/blueapi
spec:
type: openapi
lifecycle: production
Expand Down
86 changes: 48 additions & 38 deletions docs/user/reference/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ components:
- plans
title: PlanResponse
type: object
RunPlan:
additionalProperties: false
description: Task that will run a plan
properties:
name:
description: Name of plan to run
title: Name
type: string
params:
description: Values for parameters to plan, if any
title: Params
type: object
required:
- name
title: RunPlan
type: object
TaskResponse:
additionalProperties: false
description: Acknowledgement that a task has started, includes its ID
Expand Down Expand Up @@ -118,13 +134,25 @@ components:
type: string
info:
title: BlueAPI Control
version: 0.1.0
version: 0.0.2
openapi: 3.0.2
paths:
/device/{name}:
/devices:
get:
description: Retrieve information about all available devices.
operationId: get_devices_devices_get
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/DeviceResponse'
description: Successful Response
summary: Get Devices
/devices/{name}:
get:
description: Retrieve information about a devices by its (unique) name.
operationId: get_device_by_name_device__name__get
operationId: get_device_by_name_devices__name__get
parameters:
- in: path
name: name
Expand All @@ -146,22 +174,22 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Get Device By Name
/devices:
/plans:
get:
description: Retrieve information about all available devices.
operationId: get_devices_devices_get
description: Retrieve information about all available plans.
operationId: get_plans_plans_get
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/DeviceResponse'
$ref: '#/components/schemas/PlanResponse'
description: Successful Response
summary: Get Devices
/plan/{name}:
summary: Get Plans
/plans/{name}:
get:
description: Retrieve information about a plan by its (unique) name.
operationId: get_plan_by_name_plan__name__get
operationId: get_plan_by_name_plans__name__get
parameters:
- in: path
name: name
Expand All @@ -183,41 +211,23 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Get Plan By Name
/plans:
get:
description: Retrieve information about all available plans.
operationId: get_plans_plans_get
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PlanResponse'
description: Successful Response
summary: Get Plans
/task/{name}:
put:
description: Submit a task onto the worker queue.
operationId: submit_task_task__name__put
parameters:
- in: path
name: name
required: true
schema:
title: Name
type: string
/tasks:
post:
description: Submit a task to the worker.
operationId: submit_task_tasks_post
requestBody:
content:
application/json:
example:
detectors:
- x
name: count
params:
detectors:
- x
schema:
title: Task
type: object
$ref: '#/components/schemas/RunPlan'
required: true
responses:
'200':
'201':
content:
application/json:
schema:
Expand Down
33 changes: 23 additions & 10 deletions src/blueapi/service/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from contextlib import asynccontextmanager
from typing import Any, Mapping

from fastapi import Body, Depends, FastAPI, HTTPException
from fastapi import Body, Depends, FastAPI, HTTPException, Request, Response

from blueapi.config import ApplicationConfig
from blueapi.worker import RunPlan, WorkerState

from .handler import Handler, get_handler, setup_handler, teardown_handler
from .model import DeviceModel, DeviceResponse, PlanModel, PlanResponse, TaskResponse

REST_API_VERSION = "0.0.2"


@asynccontextmanager
async def lifespan(app: FastAPI):
Expand All @@ -23,6 +24,7 @@ async def lifespan(app: FastAPI):
on_shutdown=[teardown_handler],
title="BlueAPI Control",
lifespan=lifespan,
version=REST_API_VERSION,
)


Expand All @@ -34,7 +36,7 @@ def get_plans(handler: Handler = Depends(get_handler)):
)


@app.get("/plan/{name}", response_model=PlanModel)
@app.get("/plans/{name}", response_model=PlanModel)
def get_plan_by_name(name: str, handler: Handler = Depends(get_handler)):
"""Retrieve information about a plan by its (unique) name."""
try:
Expand All @@ -54,7 +56,7 @@ def get_devices(handler: Handler = Depends(get_handler)):
)


@app.get("/device/{name}", response_model=DeviceModel)
@app.get("/devices/{name}", response_model=DeviceModel)
def get_device_by_name(name: str, handler: Handler = Depends(get_handler)):
"""Retrieve information about a devices by its (unique) name."""
try:
Expand All @@ -63,20 +65,24 @@ def get_device_by_name(name: str, handler: Handler = Depends(get_handler)):
raise HTTPException(status_code=404, detail="Item not found")


@app.put("/task/{name}", response_model=TaskResponse)
@app.post("/tasks", response_model=TaskResponse, status_code=201)
def submit_task(
name: str,
task: Mapping[str, Any] = Body(..., example={"detectors": ["x"]}),
request: Request,
response: Response,
task: RunPlan = Body(
..., example=RunPlan(name="count", params={"detectors": ["x"]})
),
handler: Handler = Depends(get_handler),
):
"""Submit a task onto the worker queue."""
task_id = handler.worker.submit_task(RunPlan(name=name, params=task))
"""Submit a task to the worker."""
task_id: str = handler.worker.submit_task(task)
response.headers["Location"] = f"{request.url}/{task_id}"
handler.worker.begin_task(task_id)
return TaskResponse(task_id=task_id)


@app.get("/worker/state")
async def get_state(handler: Handler = Depends(get_handler)) -> WorkerState:
def get_state(handler: Handler = Depends(get_handler)) -> WorkerState:
"""Get the State of the Worker"""
return handler.worker.state

Expand All @@ -86,3 +92,10 @@ def start(config: ApplicationConfig):

app.state.config = config
uvicorn.run(app, host=config.api.host, port=config.api.port)


@app.middleware("http")
async def add_api_version_header(request: Request, call_next):
response = await call_next(request)
response.headers["X-API-Version"] = REST_API_VERSION
return response
11 changes: 6 additions & 5 deletions tests/service/test_rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class MyModel(BaseModel):
plan = Plan(name="my-plan", model=MyModel)

handler.context.plans = {"my-plan": plan}
response = client.get("/plan/my-plan")
response = client.get("/plans/my-plan")

assert response.status_code == 200
assert response.json() == {"name": "my-plan"}
Expand Down Expand Up @@ -65,7 +65,7 @@ class MyDevice:
device = MyDevice("my-device")

handler.context.devices = {"my-device": device}
response = client.get("/device/my-device")
response = client.get("/devices/my-device")

assert response.status_code == 200
assert response.json() == {
Expand All @@ -75,13 +75,14 @@ class MyDevice:


def test_put_plan_submits_task(handler: Handler, client: TestClient) -> None:
task_json = {"detectors": ["x"]}
task_name = "count"
task_params = {"detectors": ["x"]}
task_json = {"name": task_name, "params": task_params}

client.put(f"/task/{task_name}", json=task_json)
client.post("/tasks", json=task_json)

assert handler.worker.get_pending_tasks()[0].task == RunPlan(
name=task_name, params=task_json
name=task_name, params=task_params
)


Expand Down