Skip to main content

CLI client for interacting with Qestit QRM data from LumenRadio tooling.

Project description

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-url must 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
ALERT, IO, MEMORY, PROGRAMMING passfail Simple pass/fail checks
OUTPUT_POWER, RX_BER, TX_BER double Numeric measurements with limits
VOLTAGE double if --measurement given, otherwise passfail Power-supply checks
SETUP, TEARDOWN, SLEEP, LABEL_PRINTING Not logged to QRM
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" \
    --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 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
        }
      ]
    }
  ]
}
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_USERNAME and QRM_PASSWORD to 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.md in the current directory
  • VSCode — sets github.copilot.chat.codeGeneration.useInstructionFiles: true in .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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lr_qrm-0.8.1.tar.gz (73.1 kB view details)

Uploaded Source

Built Distribution

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

lr_qrm-0.8.1-py3-none-any.whl (46.2 kB view details)

Uploaded Python 3

File details

Details for the file lr_qrm-0.8.1.tar.gz.

File metadata

  • Download URL: lr_qrm-0.8.1.tar.gz
  • Upload date:
  • Size: 73.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for lr_qrm-0.8.1.tar.gz
Algorithm Hash digest
SHA256 0ebcc02d539f19397d462ea7fdf99347652d11a73c38fb825843c1531b85725c
MD5 8d027fb97e4975279deb84f0bf56b0c3
BLAKE2b-256 d793f1f8180b67cecea0393cb1f49d34bb5f22db01454f670457442d68334ee1

See more details on using hashes here.

File details

Details for the file lr_qrm-0.8.1-py3-none-any.whl.

File metadata

  • Download URL: lr_qrm-0.8.1-py3-none-any.whl
  • Upload date:
  • Size: 46.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for lr_qrm-0.8.1-py3-none-any.whl
Algorithm Hash digest
SHA256 20cdea553b920ea63d6b3a9c588289a48707f0ae9611a73039d08b7f7348ef08
MD5 4a195f3e3008dbc5f755e0589adc2c25
BLAKE2b-256 f98aeee376e806097107c95c8db8aa615a0bd65b523e20bf0b9a0153387b5707

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page