Libby: a tiny messaging library which uses Bamboo with pluggable transports (ZMQ or RabbitMQ)
To install the package in editable mode (ideal for development), follow these steps:
- Python 3.7 or higher
pip(ensure it's the latest version)setuptools42 or higher (for building the package)
First, clone the repository to your local machine:
git https://github.com/CaltechOpticalObservatories/libby
cd libbyCreate a virtual environment for your package:
python -m venv venv
source venv/bin/activateMake sure setuptools and pip are up to date:
pip install --upgrade pip setuptools wheelTo install your package in editable mode for development, use the following command:
pip install -e .This will install the package, allowing you to edit it directly and have changes take effect immediately without reinstalling.
To install any optional dependencies, such as development dependencies, use:
pip install -e .[dev]python -m unittest discover -s testsMost of tests/ needs no transport at all. tests/test_client_integration.py
is the exception: it starts a real LibbyDaemon over RabbitMQ and exercises
Client against it, and skips itself automatically if no broker is reachable
at amqp://localhost.
A keyword is a typed named value served over libby, with a uniform payload convention:
{}→ show (return current value){"value": V}→ modify (apply, then return it)
Types: BoolKeyword, IntKeyword, FloatKeyword, StringKeyword,
TriggerKeyword. Access mode is inferred — pass a getter for
read-only, a setter for write-only, both for read-write. Optional
extras: units, description, nullable, validator, timeout_s
(advertised in keys.describe; the CLI uses it to extend the modify
timeout for slow operations like motion).
Each Libby peer carries a keyword_registry with typed builder
methods. Build keywords by calling lib.keyword_registry.<type>(...),
then flush them to the peer with register_keywords:
from libby import Libby
libby = Libby.rabbitmq(self_id="my-peer", rabbitmq_url="amqp://localhost")
state = {"position": 0.0}
libby.keyword_registry.bool("online", getter=lambda: True)
libby.keyword_registry.float("position",
getter=lambda: state["position"],
setter=lambda v: state.update(position=v),
units="mm")
libby.keyword_registry.trigger("halt", action=lambda: print("halted"))
libby.register_keywords(libby.keyword_registry.drain())You can also build keywords directly via BoolKeyword(...) /
FloatKeyword(...) etc. and pass a list to register_keywords. The
registry is a convenience layer over the same type classes.
Clients call the keyword by name:
client = Libby.rabbitmq(self_id="client", rabbitmq_url="amqp://localhost")
client.rpc("my-peer", "position", {}) # show
client.rpc("my-peer", "position", {"value": 12.5}) # modify
client.rpc("my-peer", "halt", {"value": 1}) # fireTwo meta-services are auto-registered on every peer that uses the keyword registry:
keys.list— payload{"pattern": "..."}(default"%") → names, sorted.%wildcards within a single name.keys.describe— payload{"name": "..."}→ flat metadata dict. Exact lookup; no wildcards.
LibbyDaemon subclasses also get a lasterror keyword for free (not just
any keyword-registry user, since it needs the daemon's own logger): a
nullable string holding the most recent self.logger.error(...) message, so
a failure that only got logged locally is still visible to a remote
libby show <peer>.lasterror. Write null to clear it.
Client is the programmatic front for reading and writing keywords — the
import-and-use counterpart to the CLI. Where the CLI opens a connection per
command, a Client holds one for its lifetime, so a script can touch many
keywords cheaply. It addresses keywords by the same qualified
<group>.<scope>.<name> and reuses the CLI's cli_config.yaml.
from libby import Client
with Client.from_config() as client: # transport/url from cli_config.yaml
pos = client.get("hsfei.focpupsel.positionvalue") # -> 7.15
full = client.show("hsfei.focpupsel.positionvalue") # -> {"ok": True, "value": 7.15, "units": "mm", ...}
client.set("hsfei.pickoff.softmax", 120) # returns the applied valueConstruct explicitly when you don't want config-file resolution:
client = Client.rabbitmq(rabbitmq_url="amqp://user:pass@host")
client = Client.zmq(address_book={"hsfei_pickoff": "tcp://host:5555"})get(name)→ the value;show(name)→ the full response dict (value, units, flags);set(name, value)→ the value the daemon applied.- Failures raise rather than return sentinels:
KeywordErrorwhen the daemon rejects a get/set (its message is on.error),LibbyTimeoutwhen a request isn't answered, both subclasses ofLibbyError.setacceptstimeout_s=; otherwise it honors the keyword'stimeout_smetadata, like the CLI.
from libby import KeywordError
try:
client.get("hsfei.yjpiaagim.positionvaluex")
except KeywordError as ex:
print(ex.error) # "Control loops are not closed"Exact names only for now; % wildcard reads, list, and describe are planned
follow-ons — use the CLI for those today.
libby is the command-line front for keyword peers. Verbs:
libby show <group>.<scope>.<name> # read a keyword (% wildcard in name)
libby modify <group>.<scope>.<name>=V # write a keyword (exact name)
libby list <group>.<scope>.<pattern> # list keyword names (% wildcard in name)
libby describe <group>.<scope>.<name> # metadata for one keyword (exact name)
<group>.<scope> is the address of one peer: group is that peer's
group_id, scope is its peer_id (e.g. peer_id: adc, group_id: hsfei
in the daemon's config is addressed as hsfei.adc). Libby.rabbitmq() /
Libby.zmq() build the actual wire identity from those two fields via
libby.naming.qualified_peer_id - a daemon config never needs to
concatenate them by hand. Cross-peer fanout is not supported. req and
sub are kept for raw RPC / topic debugging.
$ libby show hsfei.pickoff.positionvalue
hsfei.pickoff.positionvalue = 79.0 mm
$ libby show hsfei.pickoff.is%
hsfei.pickoff.isconnected = True
hsfei.pickoff.isloopclosed = True
hsfei.pickoff.ismoving = False
hsfei.pickoff.isreferenced = True
$ libby modify hsfei.pickoff.softmax=120
hsfei.pickoff.softmax = 120.0 mm
$ libby modify hsfei.pickoff.softmax=null # or hsfei.pickoff.softmax=
hsfei.pickoff.softmax = None mm
$ libby describe hsfei.pickoff.positionvalue
hsfei.pickoff.positionvalue:
type float
readonly False
writeonly False
nullable False
units mm
description Stage position in engineering units.
$ libby list hsfei.pickoff.%min
hsfei.pickoff.hardmin
hsfei.pickoff.softmin
Add --json to any verb for machine-readable output (objects for
show / modify / describe, list of objects for show <pattern>,
list of strings for list).
key=valueorkey value(positional) both work.- Empty (
key=) andnullclear nullable values. - Coercion is heuristic:
true/false→ bool, integer-looking → int, decimal-looking → float, else string. - The CLI consults
keys.describefor the keyword'stimeout_smetadata before sending the modify, so slow operations (e.g. stage motion) get a longer wait automatically.--timeout <s>overrides.
The CLI looks for ~/.libby/cli_config.yaml by default; override the
path per call with --config <path>. An example template ships with
the package at libby/cli/cli_config.example.yaml — copy it and
edit:
mkdir -p ~/.libby
cp $(python -c "import libby.cli, os; print(os.path.dirname(libby.cli.__file__))")/cli_config.example.yaml ~/.libby/cli_config.yamlSchema:
transport: rabbitmq # zmq | rabbitmq
rabbitmq_url: amqp://localhost
# Used only when transport=zmq:
peers:
hsfei_pickoff: tcp://hispec.caltech.edu:5555All keys are optional. Missing file is fine — defaults are
transport: rabbitmq / rabbitmq_url: amqp://localhost.
Precedence: --transport / --rabbitmq-url flags override yaml; yaml
overrides built-in defaults. Flags must appear after the subcommand
(libby show --transport zmq foo, not the other way).
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | argument / parse error |
| 2 | RPC or response error (e.g. read-only, unknown keyword, transport failure) |
| 3 | wildcard list / show matched no keywords |