QRM
CLI + Python client for interacting with the Qestit QRM.
Install
pip install lr-qrm
Quick start
Login
By default, qrm login runs the browser OAuth flow (Azure Entra ID,
OAuth 2.0 / OIDC with Authorization Code + PKCE):
qrm login --base-url https://qrm.example.com/qrmapi
This opens your system browser to sign in against Azure Entra ID, then completes the flow via a local loopback callback.
--base-urlmust point at the API root, including the backend's route prefix. Deployments serve the API under a route prefix (for example/qrmapi); use the base URL your administrator provides. Omitting the prefix logs in successfully but every subsequent command returns a 404 ("Failed to contact QRM ... endpoint"), because the base URL stored at login time is reused by all commands.
For environments where browser login isn't possible (e.g. CI/CD, headless
machines), the legacy username/password login is available as a
fallback. Pass -u/-p directly, or set QRM_USERNAME/QRM_PASSWORD:
qrm login -u "<insert username>" -p "<insert password>" --base-url https://qrm.example.com/qrmapi
export QRM_USERNAME="<insert username>"
export QRM_PASSWORD="<insert password>"
qrm login --base-url https://qrm.example.com/qrmapi
Use --oauth to force the browser flow even when QRM_USERNAME/
QRM_PASSWORD are set:
qrm login --oauth --base-url https://qrm.example.com/qrmapi
The OAuth flow can be tuned with --client-id/--tenant-id/--scope
(or the QRM_CLIENT_ID/QRM_TENANT_ID/QRM_SCOPE env vars). These all
default to the values configured for your organization, so most users
never need to set them.
By default, login stores session details at:
~/.config/qrm/login.json
Override the location with --config-path. OAuth logins also store a
refresh_token; the access token is silently refreshed using it on the
next command when it's near expiry — you shouldn't normally need to
re-run qrm login because of token expiry.
Commands
Check API health
qrm status
Query production test results
By date range
qrm query results --start "2026-03-01T00:00:00Z" --stop "2026-03-01T23:59:59Z"
qrm query results --output json
Retrieves all production test results in the given date range (defaults to today if not specified). Output is a rich table by default, or JSON with --output json. References (operator, station, test program, etc.) are resolved for readability.
First-Pass Yield by Station × Sequence
qrm query fpy
qrm query fpy --start "2026-03-20T00:00:00" --stop "2026-03-26T23:59:59"
qrm query fpy --output json
Computes First-Pass Yield (FPY) grouped by Station and Sequence for the given date range (defaults to the last 7 days). Each row shows Total runs, Passed, Failed, and FPY % for the station/sequence pair. Use --output json for scripting.
Test Duration Statistics by Station × Sequence
qrm query test-duration
qrm query test-duration --start "2026-03-20T00:00:00" --stop "2026-03-26T23:59:59"
qrm query test-duration --output json
Aggregates test duration statistics grouped by Station and Sequence for the given date range (defaults to the last 7 days). Each row shows Count, Min (s), Avg (s), Max (s), and a Short (<10s) counter that flags runs likely to have aborted early or experienced a fixture issue. Use --output json for scripting; the envelope includes start and stop metadata.
By serial number
qrm query serial 326115020010F2F1
qrm query serial 326115020010F2F1 --output json
Retrieves all test results for a given serial number. Output is a rich table by default, or JSON with --output json. References are resolved.
All query subcommands are read-only and accept global options like --config-path, --base-url, --insecure, and --output. Query commands are compatible with both the SQL Server 2014 and PostgreSQL 17 backends.
Delete result sets for a serial
Use this to clean up test data after integration testing.
# Preview what would be deleted without making any changes
qrm uut delete-results LRQRM-TEST-001 --dry-run --insecure
# Delete all result sets for the serial (requires explicit confirmation)
qrm uut delete-results LRQRM-TEST-001 --confirm --insecure
--dry-run renders the result tree and reports how many sets would be removed without touching the database. Omitting --confirm exits with code 1 and a warning — no data is ever deleted by accident.
Uploading test results
lr-qrm provides a git-like workflow for accumulating test steps and uploading them to QRM, as well as a direct file-based upload path.
Step types and their data types
--type |
DataType in QRM | Use for |
|---|---|---|
OUTPUT_POWER, RX_BER, TX_BER |
double |
Numeric measurements with limits |
ALERT, IO, MEMORY, PROGRAMMING, VOLTAGE |
double for numeric value, string for text_value, otherwise passfail |
Pass/fail checks, optionally backed by measurements |
SETUP, TEARDOWN, SLEEP, LABEL_PRINTING |
— | Not logged to QRM |
Naming a run vs. naming a sequence
--sequence-name names this individual run — typically a unique run id, one per execution — and populates QRM's ResultSetName and TestDescription columns, while --test-sequence names the stable test sequence being executed (for example production-test or retest) and populates QRM's SequenceName column. QRM auto-creates the sequence if the name is unknown; pair --test-sequence with --test-sequence-version to record an optional version string for it. Omitting --test-sequence leaves SequenceName null, so sequences run against the same article are distinguishable in QRM only by their group counts.
Example 1 — simple pass/fail test (IO check + programming step)
# Step 1: add individual test steps to staging
qrm result add --type IO --name "USB enumeration" --outcome Passed \
--start "2026-04-23T08:00:00Z" --stop "2026-04-23T08:00:03Z"
qrm result add --type PROGRAMMING --name "Flash firmware 1.2.3" --outcome Passed \
--start "2026-04-23T08:00:03Z" --stop "2026-04-23T08:00:45Z"
qrm result add --type ALERT --name "Boot check" --outcome Passed \
--start "2026-04-23T08:00:45Z" --stop "2026-04-23T08:00:50Z"
# Step 2: commit — serial number is provided here because it may be generated during test
qrm result commit \
--serial "326115020010F2F1" \
--uut-type-name "LR5110" \
--sequence-name "LR5110 factory test" \
--test-sequence "production-test" \
--test-sequence-version "1.0.0" \
--article-revision "A" \
--station-id "STATION-001" \
--operator-id "operator" \
--location "LumenRadio" \
--test-program "OpenHTF" \
--test-program-version "1.0.0"
# Step 3: push all committed records to QRM (can accumulate multiple before pushing)
qrm result push --insecure
Example 2b — PROGRAMMING step with firmware version as a string result
qrm result add --type PROGRAMMING --name "Flash firmware" --outcome Passed \
--start "2026-04-23T09:00:12Z" --stop "2026-04-23T09:00:45Z" \
--measurement '{"name":"firmware_version","text_value":"1.2.3"}'
qrm result commit \
--serial "326115020010F2F3" \
--uut-type-name "MWA-N2" \
--sequence-name "MWA-N2 programming" \
--article-revision "B" \
--station-id "RF-STATION-001" \
--operator-id "operator" \
--location "LumenRadio" \
--test-program "OpenHTF" \
--test-program-version "1.0.0"
qrm result push --insecure
Example 2 — RF output power measurement with numeric limits
# Add a measurement step with GELE (lower and upper) limits
qrm result add --type OUTPUT_POWER --name "TX channel 1" --outcome Passed \
--start "2026-04-23T09:00:00Z" --stop "2026-04-23T09:00:05Z" \
--measurement '{"name":"power_dBm","value":-3.5,"unit":"dBm","comparator":"GELE","limit_min":-8.0,"limit_max":0.0}'
# Multiple measurements on the same step are allowed
qrm result add --type OUTPUT_POWER --name "TX channel 2" --outcome Passed \
--start "2026-04-23T09:00:05Z" --stop "2026-04-23T09:00:10Z" \
--measurement '{"name":"power_dBm","value":-4.1,"unit":"dBm","comparator":"GELE","limit_min":-8.0,"limit_max":0.0}' \
--measurement '{"name":"frequency_error_ppm","value":1.2,"unit":"ppm","comparator":"GELE","limit_min":-10.0,"limit_max":10.0}'
# GE comparator (lower bound only) — upper limit becomes "NaN" in QRM
qrm result add --type VOLTAGE --name "3.3V rail" --outcome Passed \
--start "2026-04-23T09:00:10Z" --stop "2026-04-23T09:00:11Z" \
--measurement '{"name":"voltage_V","value":3.31,"unit":"V","comparator":"GE","limit_min":3.1}'
qrm result commit \
--serial "326115020010F2F2" \
--uut-type-name "MWA-N2" \
--sequence-name "MWA-N2 RF test" \
--article-revision "B" \
--station-id "RF-STATION-001" \
--operator-id "operator" \
--location "LumenRadio" \
--test-program "OpenHTF" \
--test-program-version "1.0.0"
qrm result push --insecure
Example 3 — test multiple units before pushing (offline-friendly)
# Unit A
qrm result add --type IO --name "USB check" --outcome Passed
qrm result add --type ALERT --name "Self test" --outcome Failed
qrm result commit --serial SN-A --uut-type-name LR5110 \
--sequence-name "factory test" --article-revision A \
--station-id ST-1 --operator-id operator --location LR \
--test-program OpenHTF --test-program-version 1.0
# Unit B — starts fresh, previous commit cleared staging
qrm result add --type IO --name "USB check" --outcome Passed
qrm result add --type ALERT --name "Self test" --outcome Passed
qrm result commit --serial SN-B --uut-type-name LR5110 \
--sequence-name "factory test" --article-revision A \
--station-id ST-1 --operator-id operator --location LR \
--test-program OpenHTF --test-program-version 1.0
# Both units pushed in one go — works even after reconnecting to the network
qrm result push --insecure
# Check staging + queue state at any time
qrm result status
Direct file upload
Construct a ResultUploadPayload JSON file and upload it in one step. Useful for scripting or replaying saved results:
# Validate the file without uploading
qrm result upload result.json --dry-run
# Upload immediately (bypasses the staging/queue workflow)
qrm result upload result.json --insecure
The JSON format mirrors the Python ResultUploadPayload model:
{
"serial": "326115020010F2F1",
"uut_type_name": "LR5110",
"sequence_name": "LR5110 factory test",
"article_revision": "A",
"station_id": "STATION-001",
"operator_id": "operator",
"location": "LumenRadio",
"test_program": "OpenHTF",
"test_program_version": "1.0.0",
"outcome": "Passed",
"start_datetime": "2026-04-23T08:00:00Z",
"stop_datetime": "2026-04-23T08:00:50Z",
"uut_settings": {"firmware": "1.2.3"},
"steps": [
{
"step_type": "IO",
"name": "USB enumeration",
"outcome": "Passed",
"start_datetime": "2026-04-23T08:00:00Z",
"stop_datetime": "2026-04-23T08:00:03Z",
"measurements": []
},
{
"step_type": "OUTPUT_POWER",
"name": "TX channel 1",
"outcome": "Passed",
"start_datetime": "2026-04-23T08:00:03Z",
"stop_datetime": "2026-04-23T08:00:08Z",
"measurements": [
{
"name": "power_dBm",
"value": -3.5,
"unit": "dBm",
"comparator": "GELE",
"limit_min": -8.0,
"limit_max": 0.0
}
]
},
{
"step_type": "PROGRAMMING",
"name": "Flash firmware",
"outcome": "Passed",
"start_datetime": "2026-04-23T08:00:08Z",
"stop_datetime": "2026-04-23T08:00:50Z",
"measurements": [
{
"name": "firmware_version",
"text_value": "1.2.3"
}
]
}
]
}
Python API
from datetime import datetime, timezone
from qrm.config import load_login_state
from qrm.client import QrmClient
from qrm.step_result import StepResult, Measurement
state = load_login_state()
client = QrmClient(base_url=str(state.base_url), verify_tls=state.verify_tls)
result = client.upload_result(
token=state.token,
serial="326115020010F2F1",
uut_type_name="LR5110",
sequence_name="LR5110 factory test",
article_revision="A",
station_id="STATION-001",
operator_id="operator",
location="LumenRadio",
test_program="OpenHTF",
test_program_version="1.0.0",
outcome="Passed",
start_datetime=datetime(2026, 4, 23, 8, 0, 0, tzinfo=timezone.utc),
stop_datetime=datetime(2026, 4, 23, 8, 0, 50, tzinfo=timezone.utc),
uut_settings={"firmware": "1.2.3"},
steps=[
StepResult(
step_type="IO",
name="USB enumeration",
outcome="Passed",
start_datetime=datetime(2026, 4, 23, 8, 0, 0, tzinfo=timezone.utc),
stop_datetime=datetime(2026, 4, 23, 8, 0, 3, tzinfo=timezone.utc),
),
StepResult(
step_type="OUTPUT_POWER",
name="TX channel 1",
outcome="Passed",
start_datetime=datetime(2026, 4, 23, 8, 0, 3, tzinfo=timezone.utc),
stop_datetime=datetime(2026, 4, 23, 8, 0, 8, tzinfo=timezone.utc),
measurements=[
Measurement(
name="power_dBm",
value=-3.5,
unit="dBm",
comparator="GELE",
limit_min=-8.0,
limit_max=0.0,
)
],
),
],
)
Production Box Management
List production boxes
qrm box list --stop "2026-02-06T23:59:00Z"
If you omit --start, the command defaults to the Unix epoch (1970-01-01T00:00:00Z). Leave --stop out to let QRM use its current time.
Get detailed box information
qrm box get BOX-12345
Shows detailed information about a specific box and lists all items it contains.
Create a production box
qrm box create BOX-12345 \
--started "2026-02-01T00:00:00Z" \
--finished "2026-02-06T23:59:00Z" \
--units 10 \
--shipped
Creates or updates a production box with manufacturing dates and unit count. Use --shipped to mark the box as shipped.
Update an existing box
qrm box update BOX-12345 --units 12 --finished "2026-02-07T12:00:00Z"
Updates properties of an existing production box.
Delete a production box
qrm box delete BOX-12345
Deletes a production box. This command is idempotent and will not fail if the box is already deleted.
Add an item to a box
qrm box add-item BOX-12345 --serial-number "326115020010F2F1"
Adds an item to a production box. You can identify the item by serial number, type name, or identifier tag.
Remove an item from a box
qrm box remove-item BOX-12345 --serial-number "326115020010F2F1"
Removes an item from a production box. Supports serial number, type name, or identifier tag for identification.
Find a box containing a specific item
qrm box find --serial-number "326115020010F2F1"
Searches for the production box containing a specific item by serial number, type name, or identifier tag.
JSON output
Most commands support a JSON output mode.
qrm uut status 326115020010F2F1 --output json
Programmatic use
from qrm.config import load_login_state
from qrm.client import QrmClient
state = load_login_state()
client = QrmClient(base_url=str(state.base_url), verify_tls=state.verify_tls)
uut_runs = client.uut_status(
token=state.token,
serial_number="326115020010F2F1",
start_datetime="2026-02-01T00:00:00Z",
stop_datetime="2026-02-06T23:59:00Z",
max_results=1000,
)
boxes = client.box_list(
token=state.token,
start_datetime="1970-01-01T00:00:00Z",
stop_datetime="2026-02-06T23:59:00Z",
)
# Get detailed box information
box = client.box_get(
token=state.token,
box_identifier="BOX-12345",
)
content = client.box_content(
token=state.token,
box_identifier="BOX-12345",
)
# Create a production box
new_box = client.box_add(
token=state.token,
box_identifier="BOX-12345",
started_datetime="2026-02-01T00:00:00Z",
finished_datetime="2026-02-06T23:59:00Z",
number_of_units=10,
shipped=True,
)
# Delete a production box
result = client.box_remove(
token=state.token,
box_identifier="BOX-12345",
)
# Add an item to a box
result = client.box_uut_add(
token=state.token,
box_identifier="BOX-12345",
uut_serial_number="326115020010F2F1",
)
# Remove an item from a box
result = client.box_uut_remove(
token=state.token,
box_identifier="BOX-12345",
uut_serial_number="326115020010F2F1",
)
# Find which box contains a specific item
box = client.box_find(
token=state.token,
uut_serial_number="326115020010F2F1",
)
# Delete all result sets for a serial (e.g. integration test cleanup)
client.delete_result_sets(token=state.token, serial_number="LRQRM-TEST-001")
FAQ
-
Where is the config kept?
~/.config/qrm/login.json(override with--config-path) -
How do I run non-interactively (e.g. CI/CD)? Set
QRM_USERNAMEandQRM_PASSWORDto use the legacy local-account login instead of the interactive browser OAuth flow.
AI assistant integration
lr-qrm ships a bundled skill/instruction file that teaches AI coding assistants
about the CLI commands, Python API, data models, and common workflows.
Run this once from the root of your project:
qrm --install-skill
This does three things:
- Claude Code — copies the skill to
~/.claude/skills/lr-qrm/(global, available in all projects) - GitHub Copilot — writes
.github/instructions/lr-qrm.instructions.mdin the current directory - VSCode — sets
github.copilot.chat.codeGeneration.useInstructionFiles: truein.vscode/settings.json(creates the file if it doesn't exist; merges if it does)
To inspect the skill content without installing:
qrm --show-skill
Release files for lr-qrm 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lr_qrm-0.10.0.tar.gz | 79.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lr_qrm-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 126.5 kB
Release files / lr_qrm-0.10.0.tar.gz
| Download URL | lr_qrm-0.10.0.tar.gz |
|---|---|
| Size | 79.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
057af03c3f540078715f1eed9e00382fa1a683227631c5b4b8760e0d9705721a
|
|
BLAKE2b-256 checksum How to use checksums |
0aea15cb9d80f99321dd1be4ac75370fc8dbf21b915ecb86eae00299a20bb827
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / lr_qrm-0.10.0-py3-none-any.whl
| Download URL | lr_qrm-0.10.0-py3-none-any.whl |
|---|---|
| Size | 47.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c3baadcd64f2afa76a150f0ef64a1b6677548aad91bfe812405256259100c036
|
|
BLAKE2b-256 checksum How to use checksums |
3bf3a322d9083975e0666cfaded8e698e606200047e75ceb4f63d325ee53a254
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|