Metadata-Version: 2.4
Name: lr-qrm
Version: 0.7.0
Summary: CLI client for interacting with Qestit QRM data from LumenRadio tooling.
Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
License: MIT License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.32
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13.7
Requires-Dist: pydantic>=2.8
Provides-Extra: dev
Requires-Dist: build>=1.2.1; extra == "dev"
Requires-Dist: twine>=5.1.1; extra == "dev"
Requires-Dist: wheel; extra == "dev"
Requires-Dist: pytest>=8.4.2; extra == "dev"
Requires-Dist: black>=25.9.0; extra == "dev"
Requires-Dist: pytest-html; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# QRM

CLI + Python client for interacting with the Qestit QRM.

## Install

```bash
pip install lr-qrm
```

## Quick start

### Login

Interactive login (prompts for username/password):

```bash
qrm login --base-url https://example.com
```

Non-interactive (for CI/CD):

```bash
export QRM_USERNAME="<insert username>"
export QRM_PASSWORD="<insert password>"
qrm login --ci --base-url https://example.com
```

By default, this stores session details at:

```
~/.config/qrm/login.json
```

### Commands

#### Check API health

```bash
qrm status
```


#### Query production test results

##### By date range

```bash
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

```bash
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

```bash
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

```bash
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.

```bash
# 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)

```bash
# 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

```bash
# 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)

```bash
# 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:

```bash
# 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:

```json
{
  "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

```python
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

```bash
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

```bash
qrm box get BOX-12345
```

Shows detailed information about a specific box and lists all items it contains.

##### Create a production box

```bash
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

```bash
qrm box update BOX-12345 --units 12 --finished "2026-02-07T12:00:00Z"
```

Updates properties of an existing production box.

##### Delete a production box

```bash
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

```bash
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

```bash
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

```bash
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.

```bash
qrm uut status 326115020010F2F1 --output json
```

## Programmatic use

```python
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 `QRM_CONFIG`)

- **How do I run non-interactively?**
  Make sure to give all required arguments. Also pass `--ci` to stop output of sensitive information such as username or passwords.

---

## 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:

```bash
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:

```bash
qrm --show-skill
```
