This release is a pre-release and may not be stable for production use.
labcat
A small test-and-measurement device catalog: what a device is, where it currently is, and the full history of how it got there. Built to solve one specific problem: manually re-typing device IDs, models, and fixture positions into measurement scripts is tedious and an easy place to make a mistake (mounting two devices in the same fixture position without noticing, for instance).
This is a reference/inventory tool, deliberately kept separate from nebula (which handles measurement data provenance). labcat answers "what is this device and where is it"; nebula answers "what produced this data file." A measurement script typically asks labcat for device info, then hands the result to nebula as plain structured data:
dev = labcat.load().get("R4C1W8")
s.write_meta_for(fn, inputs={"device_id": dev.id, "model": dev.attributes["model"],
"position": dev.current_position()})
Data model
One YAML file is the source of truth — small, human-readable, and meant to be git-tracked (unlike a nebula archive, which should not go in git; see that project's README for why the two have opposite storage answers).
Each device has:
attributes— a small freeform dict for things that rarely change and don't follow a predictable pattern: model, manufacturer, hand-written design notes. Direct overwrite, no history tracked.events— a generic, typed, timestamped log. Current state (current position, current status, anything else) is always derived from this log, never stored redundantly, so there's exactly one place that can be wrong.placementis the built-in span-type event (start/end) — this is whatmount/unmount/movewrite.statusis the built-in instant-type event (timestamp/value).- Anything else — calibration runs, ownership changes, whatever comes
up later — is just a new
typestring vialog_event()/labcat log. No schema change required to add a new kind of thing to track.
R4C1W8:
attributes:
model: "v6"
notes: "15-2-15"
events:
- type: placement
position: 6
start: "2026-08-10"
end: "2027-08-14"
- type: status
timestamp: "2027-08-20"
value: "in-progress"
Where the file lives
labcat.load() resolves the catalog path from, in order: an explicit
path, $LABCAT_CATALOG, or ~/.labcat/catalog.yaml. Point it at a file
inside a git repo you sync across machines — that's the whole
multi-machine story; labcat itself has no networking.
Safety
- Position conflicts are a hard refusal by default.
mount()raisesPositionOccupiedif another device already occupies that position, andAlreadyMountedif the device you're mounting is already somewhere else. Passforce=Trueto override either check. This refusal is the actual point of the tool — it's the "centralized check against errors" a hand-maintained.envfile can't give you. - File-locked, reload-then-write mutations. Every mutating call
(
mount,unmount,move,log_event,add_device) takes a local file lock, re-reads the catalog fresh from disk, applies the change, and writes — so two processes on the same machine racing on a write can't silently clobber each other. This does not cover true concurrent edits from two different machines; that's git's job (the YAML is small and diffable specifically so a merge conflict here is something you can read and resolve by eye).
CLI
labcat init [PATH] [--force]
labcat add <device_id> [--model M] [--notes N] [--attr key=value ...]
labcat set <device_id> [key=value ...] [--model M] [--notes N] [--append key=value] [--remove key]
labcat mount <device_id> <position> [--date YYYY-MM-DD] [--force]
labcat unmount (<device_id> | --all) [--date YYYY-MM-DD]
labcat move <device_id> <position> [--date YYYY-MM-DD] [--force]
labcat log <device_id> <type> [key=value ...] [--date YYYY-MM-DD]
labcat status <device_id> <value> [--date YYYY-MM-DD]
labcat show <device_id>
labcat list [--mounted]
labcat whats-in [<position>]
labcat history <device_id> [--type TYPE]
labcat where
Every command accepts a global --catalog PATH, placed right after
labcat and before the subcommand, which overrides $LABCAT_CATALOG for
that one invocation:
labcat --catalog /path/to/other.yaml list
init creates an empty catalog file on disk (refuses to overwrite an
existing one unless --force) — not required, since add/mount/etc.
create the file on first write, but useful when you want something to
commit to git before you've added any devices. It takes an optional
path (labcat init ~/repos/lab-devices/catalog.yaml); when that isn't
the default location, it prints the line to add to your shell startup
file — in your shell's own syntax — to make it the default.
labcat where prints which catalog file is in use and which of the
three sources chose it. Worth knowing about: an unset $LABCAT_CATALOG
and a working catalog aren't a contradiction, they just mean the
built-in default is in play, and where is how you tell.
labcat whats-in with no position lists every occupied position — the
whole current layout. labcat unmount --all tears down every mounted
device in a single write. set edits attributes on a device that already exists — the clearest
entry point for "I want to start tracking a field I didn't capture when
I first added this device." It covers the whole attribute lifecycle:
labcat set R4C1W8 foundry=fab-b # set or overwrite
labcat set R4C1W8 --model v7 # same, for model/notes
labcat set R4C1W8 --append notes="re-bonded"
labcat set R4C1W8 --remove foundry
Combine them freely in one command; they apply as set, then append,
then remove, in a single write, so a bad --remove can't leave a
device half-updated. --append joins onto the existing value with
"; " (--sep changes it) and plain-sets an attribute that isn't
there yet. labcat show displays them. add --update remains the way
to edit while creating-or-updating in one step; set is for a device
you know already exists (and says so loudly if it doesn't). See examples/cli_walkthrough.txt for a full narrated
session covering all three of these.
Python API
import labcat
cat = labcat.load()
cat.add_device("R4C1W8", attributes={"model": "v6", "notes": "15-2-15"})
cat.mount("R4C1W8", position=6)
dev = cat.get("R4C1W8")
dev.current_position() # 6
dev.attributes["model"] # "v6"
dev.history() # full event log, oldest first
Picker for measurement scripts (optional PyQt5 dependency, not imported by default):
from labcat.picker import pick_device
dev = pick_device() # pops a picker of currently-mounted devices
# or, for non-interactive/batch use:
dev = pick_device(non_interactive_id="R4C1W8")
Examples
examples/ has a narrated CLI walkthrough (cli_walkthrough.txt,
covering --catalog, init, and adding new fields to an existing
device) and three runnable Python scripts: a basic device lookup
(lookup_device.py), a reverse lookup by fixture position
(whats_in_position.py), and the pattern for resolving a device at the
start of a measurement script and handing its info to nebula
(measurement_script_pattern.py).
Status
Core catalog model, CLI, and picker scaffold implemented and unit
tested (device lifecycle, position-conflict refusal and force
override, generic event logging, locking against same-machine race
conditions). Not yet tested against a real Qt event loop or a real
multi-machine git sync workflow.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file labcat-0.1.0.dev1.tar.gz.
File metadata
- Download URL: labcat-0.1.0.dev1.tar.gz
- Upload date:
- Size: 27.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
317a4614c80d272464de7df02bc678a13083f953b288185fd65b3f29c2406957
|
|
| MD5 |
e5b8d047070c70f9dd78434dd5142975
|
|
| BLAKE2b-256 |
7ba3bae64c84f8196dca4a68685095390b4493b7fe9c6e929eff8201bc460f14
|
File details
Details for the file labcat-0.1.0.dev1-py3-none-any.whl.
File metadata
- Download URL: labcat-0.1.0.dev1-py3-none-any.whl
- Upload date:
- Size: 21.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46d37cf5d1bc0d705ecfdfb2ddbe8f2bbca657e0f6d9e80d1eb83576176a3a6e
|
|
| MD5 |
345893888d10c7f3f21ecbdb1db29f7f
|
|
| BLAKE2b-256 |
bdfa466c4c020201c5b21287999b40537c842e2f489d823b253591ca931007ea
|