Skip to content

Conversation

@tpoliaw
Copy link
Contributor

@tpoliaw tpoliaw commented Jan 6, 2026

Where previously the BlueapiClient was mostly a thin wrapper around the
BlueapiRestClient, it is now a more standalone class intended to be
usable from other scripts/applications as well as interactive sessions.

The client can now be created from just the path to a config file
without the user having to handle parsing.

Plans can now be run with less boilerplate with a more method-like
interface. Available devices can be queried and then used as if they
were local objects when running plans.

Using the new client

from blueapi.client.client import BlueapiClient
bc = BlueapiClient.from_config_file("/path/to/config.yaml")
bc.instrument_session = "cm12345-1"

This client can then be reused to run plans or query devices.

for dev in bc.devices:
    print(dev)
    print("    " + ", ".join(p.name for p in dev.model.protocols))

det = bc.devices.det
stage = bc.devices.stage

Sub-devices can also be accessed, while checking that the sub-device
exists on the server:

# assuming the default devices in adsim
stage_x = stage.x # returns new device
det_x = det.x # raises AttributeError

Available plans can be accessed from the server and treated
(approximately) like local methods. Calling a plan as a method blocks
while the plan is running

count = bc.plans.count
help(count) # Display docstring from plan
assert str(count) == "count(detectors, num=None, delay=None, metadata=None)"

# Arguments can be passed as positional arguments
count([det, stage.x], 3) # det and stage from previous step

# ...or by keyword
count([det, stage.x], delay=12)

# ... but the same argument can't use both
count([det], detectors=[stage.x]) # raises TypeError

Other client methods have been replaced by (cached) properties:

print(bc.environment)
print(bc.oidc_config)

if bc.state == WorkerState.IDLE
    # previous methods are mostly still available
    bc.run_task(...)
else:
    print(bc.active_task)

While running in an interactive session, plans and devices are
auto-completable if the REPL supports it.

@codecov
Copy link

codecov bot commented Jan 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.34%. Comparing base (c0821c4) to head (ba2287b).

Additional details and impacted files
@@               Coverage Diff               @@
##           find-device    #1323      +/-   ##
===============================================
+ Coverage        95.00%   95.34%   +0.34%     
===============================================
  Files               42       42              
  Lines             2761     2900     +139     
===============================================
+ Hits              2623     2765     +142     
+ Misses             138      135       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DominicOram
Copy link
Contributor

Some initial thoughts without having looked into it too much:

  • Without docs/more usage tests it's not obvious to me what the interface is supposed to look like. It looks like I can run my plan by either doing client.plans.my_plan(args) or something like client.run_task(TaskRequest(name="my_plan"...). We should only allow one and make the other private. client.plans.my_plan(args) feels quite magic to me but I do quite like it as an interface as it reads really nicely for the end scripts
  • Should: What is the difference between run_task and create_and_start_task seems to me we only need one?
  • Could: I think that for a user a task and a plan are pretty synonymous so I would change the wording so that the client just talks about plans, not tasks

@tpoliaw
Copy link
Contributor Author

tpoliaw commented Jan 8, 2026

Thanks for looking at it. You're right, it needs a lot more docs but I wanted to wait to check the interface was vaguely right before writing it up. I'll add something to the PR for now.

  • It looks like I can run my plan by either doing client.plans.my_plan(args) or something like client.run_task(TaskRequest(name="my_plan"...). We should only allow one and make the other private.

The client.plans.my_plan(client.devices.stage.x, optional=other) interface is the one added here and is the one intended for use in scripts.

The run_task/create_and_start_task methods were there before and I didn't want to break backwards compatibility too much. Maybe if we're changing it anyway, breaking everything at once isn't the end of the world.

  • Should: What is the difference between run_task and create_and_start_task seems to me we only need one?

run_task blocks, create_and_start doesn't

  • Could: I think that for a user a task and a plan are pretty synonymous so I would change the wording so that the client just talks about plans, not tasks

Not a bad idea but all the task related things were here before this PR so should be a separate change. Could be rolled into #1080 as that already removes the TaskRequest parameters

@tpoliaw tpoliaw changed the title WIP User client feat!: Redesign BlueapiClient for use in scripts Jan 13, 2026
@tpoliaw tpoliaw marked this pull request as ready for review January 13, 2026 16:42
@tpoliaw tpoliaw requested a review from a team as a code owner January 13, 2026 16:42
@tpoliaw
Copy link
Contributor Author

tpoliaw commented Jan 14, 2026

This requires #1315

@tpoliaw tpoliaw changed the base branch from main to find-device January 14, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants