Skip to main content
Pre-release

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.
    • placement is the built-in span-type event (start/end) — this is what mount/unmount/move write.
    • status is the built-in instant-type event (timestamp/value).
    • Anything else — calibration runs, ownership changes, whatever comes up later — is just a new type string via log_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() raises PositionOccupied if another device already occupies that position, and AlreadyMounted if the device you're mounting is already somewhere else. Pass force=True to override either check. This refusal is the actual point of the tool — it's the "centralized check against errors" a hand-maintained .env file 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

labcat-0.1.0.dev1.tar.gz (27.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

labcat-0.1.0.dev1-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

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

Hashes for labcat-0.1.0.dev1.tar.gz
Algorithm Hash digest
SHA256 317a4614c80d272464de7df02bc678a13083f953b288185fd65b3f29c2406957
MD5 e5b8d047070c70f9dd78434dd5142975
BLAKE2b-256 7ba3bae64c84f8196dca4a68685095390b4493b7fe9c6e929eff8201bc460f14

See more details on using hashes here.

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

Hashes for labcat-0.1.0.dev1-py3-none-any.whl
Algorithm Hash digest
SHA256 46d37cf5d1bc0d705ecfdfb2ddbe8f2bbca657e0f6d9e80d1eb83576176a3a6e
MD5 345893888d10c7f3f21ecbdb1db29f7f
BLAKE2b-256 bdfa466c4c020201c5b21287999b40537c842e2f489d823b253591ca931007ea

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0.dev1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page