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.

Metadata

Release files for labcat 0.1.0.dev1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for labcat 0.1.0.dev1
File Size Uploaded Provenance Actions
labcat-0.1.0.dev1.tar.gz 27.9 kB Without provenance Details for labcat-0.1.0.dev1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for labcat 0.1.0.dev1
File Interpreter ABI Platform Actions
labcat-0.1.0.dev1-py3-none-any.whl Python 3 none any Details for labcat-0.1.0.dev1-py3-none-any.whl

Total release size: 49.3 kB

Release files / labcat-0.1.0.dev1.tar.gz

Download URL labcat-0.1.0.dev1.tar.gz
Size 27.9 kB
Tags Source
SHA-256 checksum
How to use checksums
317a4614c80d272464de7df02bc678a13083f953b288185fd65b3f29c2406957
BLAKE2b-256 checksum
How to use checksums
7ba3bae64c84f8196dca4a68685095390b4493b7fe9c6e929eff8201bc460f14
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release files / labcat-0.1.0.dev1-py3-none-any.whl

Download URL labcat-0.1.0.dev1-py3-none-any.whl
Size 21.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
46d37cf5d1bc0d705ecfdfb2ddbe8f2bbca657e0f6d9e80d1eb83576176a3a6e
BLAKE2b-256 checksum
How to use checksums
bdfa466c4c020201c5b21287999b40537c842e2f489d823b253591ca931007ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Security for labcat 0.1.0.dev1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release
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