lr-lumentest-cli 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. lr_lumentest_cli-0.5.1/LICENSE +21 -0
  2. lr_lumentest_cli-0.5.1/Makefile +145 -0
  3. lr_lumentest_cli-0.5.1/PKG-INFO +245 -0
  4. lr_lumentest_cli-0.5.1/README.md +213 -0
  5. lr_lumentest_cli-0.5.1/lumentest.spec +48 -0
  6. lr_lumentest_cli-0.5.1/pyproject.toml +86 -0
  7. lr_lumentest_cli-0.5.1/setup.cfg +4 -0
  8. lr_lumentest_cli-0.5.1/src/lr_lumentest_cli.egg-info/PKG-INFO +245 -0
  9. lr_lumentest_cli-0.5.1/src/lr_lumentest_cli.egg-info/SOURCES.txt +55 -0
  10. lr_lumentest_cli-0.5.1/src/lr_lumentest_cli.egg-info/dependency_links.txt +1 -0
  11. lr_lumentest_cli-0.5.1/src/lr_lumentest_cli.egg-info/entry_points.txt +2 -0
  12. lr_lumentest_cli-0.5.1/src/lr_lumentest_cli.egg-info/requires.txt +15 -0
  13. lr_lumentest_cli-0.5.1/src/lr_lumentest_cli.egg-info/top_level.txt +1 -0
  14. lr_lumentest_cli-0.5.1/src/lumentest/__init__.py +6 -0
  15. lr_lumentest_cli-0.5.1/src/lumentest/__main__.py +3 -0
  16. lr_lumentest_cli-0.5.1/src/lumentest/_version.py +24 -0
  17. lr_lumentest_cli-0.5.1/src/lumentest/auth.py +185 -0
  18. lr_lumentest_cli-0.5.1/src/lumentest/build_words.py +94 -0
  19. lr_lumentest_cli-0.5.1/src/lumentest/cli.py +58 -0
  20. lr_lumentest_cli-0.5.1/src/lumentest/client.py +1125 -0
  21. lr_lumentest_cli-0.5.1/src/lumentest/commands/__init__.py +1 -0
  22. lr_lumentest_cli-0.5.1/src/lumentest/commands/config.py +218 -0
  23. lr_lumentest_cli-0.5.1/src/lumentest/commands/operator.py +200 -0
  24. lr_lumentest_cli-0.5.1/src/lumentest/commands/product.py +686 -0
  25. lr_lumentest_cli-0.5.1/src/lumentest/commands/result.py +692 -0
  26. lr_lumentest_cli-0.5.1/src/lumentest/commands/sequence.py +325 -0
  27. lr_lumentest_cli-0.5.1/src/lumentest/commands/test.py +308 -0
  28. lr_lumentest_cli-0.5.1/src/lumentest/commands/test_ui.py +753 -0
  29. lr_lumentest_cli-0.5.1/src/lumentest/commands_shared.py +301 -0
  30. lr_lumentest_cli-0.5.1/src/lumentest/config.py +69 -0
  31. lr_lumentest_cli-0.5.1/src/lumentest/env_report.py +220 -0
  32. lr_lumentest_cli-0.5.1/src/lumentest/manifest_gen.py +235 -0
  33. lr_lumentest_cli-0.5.1/src/lumentest/prerequisites_report.py +218 -0
  34. lr_lumentest_cli-0.5.1/tests/__init__.py +0 -0
  35. lr_lumentest_cli-0.5.1/tests/conftest.py +67 -0
  36. lr_lumentest_cli-0.5.1/tests/integration/__init__.py +0 -0
  37. lr_lumentest_cli-0.5.1/tests/integration/test_entrypoint_smoke.py +105 -0
  38. lr_lumentest_cli-0.5.1/tests/unit/__init__.py +0 -0
  39. lr_lumentest_cli-0.5.1/tests/unit/test_auth.py +314 -0
  40. lr_lumentest_cli-0.5.1/tests/unit/test_build_words.py +84 -0
  41. lr_lumentest_cli-0.5.1/tests/unit/test_cli_version.py +28 -0
  42. lr_lumentest_cli-0.5.1/tests/unit/test_client.py +561 -0
  43. lr_lumentest_cli-0.5.1/tests/unit/test_commands_shared.py +249 -0
  44. lr_lumentest_cli-0.5.1/tests/unit/test_config.py +138 -0
  45. lr_lumentest_cli-0.5.1/tests/unit/test_config_commands.py +551 -0
  46. lr_lumentest_cli-0.5.1/tests/unit/test_env_report.py +331 -0
  47. lr_lumentest_cli-0.5.1/tests/unit/test_json_output_contract.py +716 -0
  48. lr_lumentest_cli-0.5.1/tests/unit/test_manifest_gen.py +488 -0
  49. lr_lumentest_cli-0.5.1/tests/unit/test_operator.py +235 -0
  50. lr_lumentest_cli-0.5.1/tests/unit/test_prerequisites_report.py +470 -0
  51. lr_lumentest_cli-0.5.1/tests/unit/test_product.py +874 -0
  52. lr_lumentest_cli-0.5.1/tests/unit/test_product_import.py +1521 -0
  53. lr_lumentest_cli-0.5.1/tests/unit/test_pypi_metadata.py +206 -0
  54. lr_lumentest_cli-0.5.1/tests/unit/test_result.py +1309 -0
  55. lr_lumentest_cli-0.5.1/tests/unit/test_sequence.py +582 -0
  56. lr_lumentest_cli-0.5.1/tests/unit/test_test_commands.py +767 -0
  57. lr_lumentest_cli-0.5.1/tests/unit/test_test_ui.py +1141 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LumenRadio AB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ VENV := .venv
2
+ PY := $(VENV)/bin/python
3
+ PIP := $(VENV)/bin/pip
4
+
5
+ # Resolve black config from the repo root (one directory up).
6
+ # This avoids duplicating line-length / target-version between server and CLI.
7
+ ROOT_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))/..)
8
+
9
+ # setuptools_scm version, resolved on the host from the installed _version.py.
10
+ # Injected via SETUPTOOLS_SCM_PRETEND_VERSION_FOR_* so the sdist build skips the
11
+ # VCS file-finder (which root=".." would otherwise use to pull in the whole
12
+ # monorepo), and so in-container builds (no git / no repo-root mount) get it too.
13
+ DIST_ENV := SETUPTOOLS_SCM_PRETEND_VERSION_FOR_LR_LUMENTEST_CLI
14
+ SCM_VERSION = $(shell $(PY) -c "import lumentest._version as v; print(v.version)" 2>/dev/null)
15
+
16
+ # Only the sdist and wheel are valid PyPI distributions; dist/ also holds the
17
+ # PyInstaller one-file binary and the linux-arm64/ build tree, so a bare
18
+ # dist/* glob trips twine on files it was never meant to see.
19
+ DIST_FILES := dist/*.whl dist/*.tar.gz
20
+
21
+ PYPI_REPOSITORY ?= https://upload.pypi.org/legacy/
22
+ TEST_PYPI_REPOSITORY ?= https://test.pypi.org/legacy/
23
+
24
+ # The PyPI token lives in the releasing developer's shell, exported by hand
25
+ # before running `publish`/`publish-test` — the same arrangement lr-gladiator
26
+ # and lr-qrm use. No CI job holds it, so a release reaches PyPI only when a
27
+ # person runs the target.
28
+ #
29
+ # setuptools_scm derives SCM_VERSION from the `lumentest-*` tag: any commit
30
+ # past the tag carries a `.devN+g<sha>` suffix. PyPI rejects a local (`+…`)
31
+ # segment outright, and a `.dev` upload permanently spends a version number
32
+ # the index can never reuse — so both upload targets refuse anything but a
33
+ # clean tag.
34
+ CHECK_RELEASE_VERSION = case "$(SCM_VERSION)" in *dev*|*+*|"") echo "error: refusing to upload $(SCM_VERSION) — build from a clean 'lumentest-X.Y.Z' tag"; exit 2 ;; esac
35
+
36
+ .DEFAULT_GOAL := install
37
+
38
+ .PHONY: install dev test test-only format dist check publish publish-test shell clean package build-linux-docker build-linux-arm64-docker build-windows-docker
39
+
40
+ $(VENV)/bin/activate:
41
+ python3.12 -m venv $(VENV)
42
+ $(PIP) install --upgrade pip
43
+
44
+ install: $(VENV)/bin/activate
45
+ $(PIP) install .
46
+
47
+ dev: $(VENV)/bin/activate
48
+ $(PIP) install -e ".[dev,package]"
49
+
50
+ # `test` is the whole job: sync the venv, then run pytest. `test-only` is the
51
+ # pytest half on its own, for a driver that has already done the `dev` pass and
52
+ # wants to run several sub-trees at once -- the editable installs `dev` performs
53
+ # write in-tree build artefacts, so they must not overlap; pytest runs may.
54
+ test: dev test-only
55
+
56
+ test-only:
57
+ $(VENV)/bin/pytest \
58
+ --cov=lumentest \
59
+ --cov-report=term-missing \
60
+ --cov-report=html:htmlcov \
61
+ --junit-xml=test-results.xml \
62
+ tests/
63
+
64
+ format: dev
65
+ $(VENV)/bin/black --config $(ROOT_DIR)/pyproject.toml src/ tests/
66
+
67
+ # Clearing the previous sdist and wheel is what keeps `$(DIST_FILES)` meaning
68
+ # "this build": every artefact accumulates under its own version, and an upload
69
+ # globbing the directory would carry a stale version along with the current one.
70
+ # The PyInstaller binary and the arm64 tree in dist/ are left alone.
71
+ dist: dev
72
+ rm -f dist/*.whl dist/*.tar.gz
73
+ $(DIST_ENV)="$(SCM_VERSION)" $(PY) -m build
74
+
75
+ check: dist
76
+ $(VENV)/bin/twine check $(DIST_FILES)
77
+
78
+ # Both upload targets pass credentials through TWINE_USERNAME/TWINE_PASSWORD,
79
+ # which twine reads natively, and silence the upload line with `@`. `-u`/`-p`
80
+ # would put the token in the echoed recipe line and in the upload process's
81
+ # argv, where `ps` can read it — visible in a developer's scrollback and, since
82
+ # this target also runs as a CI job, in a captured job log.
83
+ publish-test: check
84
+ @$(CHECK_RELEASE_VERSION)
85
+ @if [ -z "$$TEST_PYPI_TOKEN" ]; then echo "error: TEST_PYPI_TOKEN is required — create one at https://test.pypi.org/manage/account/token/"; exit 2; fi
86
+ @TWINE_USERNAME=__token__ TWINE_PASSWORD="$$TEST_PYPI_TOKEN" $(VENV)/bin/twine upload --repository-url $(TEST_PYPI_REPOSITORY) $(DIST_FILES)
87
+
88
+ publish: check
89
+ @$(CHECK_RELEASE_VERSION)
90
+ @if [ -z "$$PYPI_TOKEN" ]; then echo "error: PYPI_TOKEN is required — create one at https://pypi.org/manage/account/token/"; exit 2; fi
91
+ @TWINE_USERNAME=__token__ TWINE_PASSWORD="$$PYPI_TOKEN" $(VENV)/bin/twine upload --repository-url $(PYPI_REPOSITORY) $(DIST_FILES)
92
+
93
+ shell:
94
+ @echo "source $(VENV)/bin/activate"
95
+
96
+ clean:
97
+ rm -rf $(VENV) dist/ build/ htmlcov/ test-results.xml .coverage
98
+ find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; true
99
+ find . -name "*.pyc" -delete 2>/dev/null; true
100
+ find . -name "*.egg-info" -type d -exec rm -rf {} + 2>/dev/null; true
101
+
102
+ package: dev
103
+ $(VENV)/bin/pyinstaller lumentest.spec
104
+
105
+ # ubuntu:24.04 caps the glibc floor at 2.39 by construction.
106
+ build-linux-docker: dev
107
+ docker run --rm \
108
+ -e $(DIST_ENV)="$(SCM_VERSION)" \
109
+ -e DEBIAN_FRONTEND=noninteractive \
110
+ -v "$(CURDIR):/src" \
111
+ -w /src \
112
+ ubuntu:24.04 \
113
+ bash -c "apt-get update -qq && \
114
+ apt-get install -y --no-install-recommends python3 python3-venv python3-pip binutils && \
115
+ python3 -m venv /build-venv && \
116
+ /build-venv/bin/pip install --no-cache-dir --upgrade pip setuptools wheel && \
117
+ /build-venv/bin/pip install --no-cache-dir -e ".[package]" && \
118
+ /build-venv/bin/pyinstaller lumentest.spec; \
119
+ rc=\$$?; chown -R $(shell id -u):$(shell id -g) build dist src/*.egg-info 2>/dev/null; exit \$$rc"
120
+
121
+ build-linux-arm64-docker: dev
122
+ docker run --rm \
123
+ --platform linux/arm64 \
124
+ -e $(DIST_ENV)="$(SCM_VERSION)" \
125
+ -e DEBIAN_FRONTEND=noninteractive \
126
+ -v "$(CURDIR):/src" \
127
+ -w /src \
128
+ ubuntu:24.04 \
129
+ bash -c "apt-get update -qq && \
130
+ apt-get install -y --no-install-recommends python3 python3-venv python3-pip binutils && \
131
+ python3 -m venv /build-venv && \
132
+ /build-venv/bin/pip install --no-cache-dir --upgrade pip setuptools wheel && \
133
+ /build-venv/bin/pip install --no-cache-dir -e ".[package]" && \
134
+ /build-venv/bin/pyinstaller lumentest.spec --distpath dist/linux-arm64 --workpath build/linux-arm64; \
135
+ rc=\$$?; chown -R $(shell id -u):$(shell id -g) build dist src/*.egg-info 2>/dev/null; exit \$$rc"
136
+
137
+ build-windows-docker: dev
138
+ docker run --rm \
139
+ --entrypoint bash \
140
+ -e $(DIST_ENV)="$(SCM_VERSION)" \
141
+ -v "$(CURDIR):/src" \
142
+ -w /src \
143
+ batonogov/pyinstaller-windows:v5.0.0 \
144
+ -c "pip install --no-cache-dir --upgrade -e . && pyinstaller lumentest.spec; \
145
+ rc=\$$?; chown -R $(shell id -u):$(shell id -g) build dist src/*.egg-info 2>/dev/null; exit \$$rc"
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: lr-lumentest-cli
3
+ Version: 0.5.1
4
+ Summary: CLI client for the LumenRadio LumenTest production test station: import artifact packages, run test sequences, and read results over HTTP.
5
+ Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/lumenradio/production/lumentest
8
+ Project-URL: Changelog, https://gitlab.com/lumenradio/production/lumentest/-/blob/main/CHANGELOG.md
9
+ Keywords: LumenRadio,LumenTest,production test,test station,CLI
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Environment :: Console
14
+ Classifier: Topic :: Software Development :: Testing
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: typer>=0.12
19
+ Requires-Dist: rich>=13.7
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: pydantic>=2.8
22
+ Requires-Dist: mnemonic>=0.21
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2.1; extra == "dev"
25
+ Requires-Dist: twine>=6.1; extra == "dev"
26
+ Requires-Dist: pytest>=8.4; extra == "dev"
27
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
28
+ Requires-Dist: black>=25.9; extra == "dev"
29
+ Provides-Extra: package
30
+ Requires-Dist: pyinstaller==6.21.0; extra == "package"
31
+ Dynamic: license-file
32
+
33
+ # lr-lumentest-cli
34
+
35
+ CLI for the LumenRadio LumenTest production test station server.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install lr-lumentest-cli
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```bash
46
+ # Point the CLI at your station server and identify yourself by scanning
47
+ # your badge (the handheld scanner types the badge id at the prompt)
48
+ lumentest login --base-url http://10.0.0.1:17080 --badge-id LOP0000001
49
+
50
+ # Check server health and login state
51
+ lumentest status
52
+
53
+ # Scan and list imported products
54
+ lumentest product scan
55
+ lumentest product list
56
+
57
+ # Run a full test sequence interactively (prompts on ALERT steps)
58
+ lumentest test start --serial SN-00001
59
+
60
+ # Run a single step (for development / debugging)
61
+ lumentest test step production-test flash-firmware --serial SN-00001
62
+ ```
63
+
64
+ ## Command Reference
65
+
66
+ ```
67
+ lumentest login [--base-url URL] --badge-id ID
68
+ # ID is the scanned QR badge; the server resolves it to your operator id.
69
+ lumentest logout
70
+ lumentest status [--output rich|json]
71
+ lumentest --version
72
+
73
+ lumentest product list [--output rich|json]
74
+ # Table includes a Build Words column — see doc/build-words.md. The manifest
75
+ # digest is not shown: read it from --output json when you need the digest.
76
+ lumentest product active
77
+ lumentest product set ARTICLE [REVISION]
78
+ lumentest product remove ARTICLE [REVISION] [--yes]
79
+ lumentest product scan [--rescan] [--output rich|json]
80
+ # Scans the station's artifact_cache/, registers every valid package, and
81
+ # reports each package's declared env variables (from parameters.toml) —
82
+ # the value a run will see, where it came from (config.toml's [env], the
83
+ # station's own environment, or the package default) and which are still
84
+ # unconfigured — alongside its prerequisites (the external tools its
85
+ # sequences need) against the station itself. Each unconfigured variable's
86
+ # line carries the exact `lumentest config set env.NAME <VALUE>` command to
87
+ # fix it; a set value applies to the running station immediately, so
88
+ # re-running scan is enough to confirm it, for a variable and for a tool
89
+ # newly installed on PATH alike — no restart needed either way. --output
90
+ # json emits the raw payload.
91
+ lumentest product import PATH [--update] [--bundle] [--output rich|json]
92
+ # PATH: directory or .zip/.tar.gz archive.
93
+ # --update: recompute SHA-256 checksums in artifact-manifest.json before
94
+ # uploading — useful during step development to skip manual hashing.
95
+ # --bundle: pack PATH into <PATH>.tar.gz (manifest + declared files only)
96
+ # next to it instead of uploading — for email/USB delivery;
97
+ # import the archive later with the same command. Requires a
98
+ # directory; passing an archive file just warns and exits.
99
+ # Prints the package's Build Words (doc/build-words.md) so a
100
+ # technician can read them off a locally built bundle without a
101
+ # running server; --output json carries them as "build_words".
102
+ # Reports the package's declared env variables the same way `product scan`
103
+ # does: the value a run will see, where it came from, and which still need
104
+ # a value. Fix an unconfigured one with the `lumentest config set` command
105
+ # printed on its line, then re-run `product scan` to confirm — same as
106
+ # confirming a newly installed prerequisite tool, neither needs a restart.
107
+ # Only artifact-manifest.json's declared files (and declared extra
108
+ # sequences) are packed and uploaded — the manifest is the contract for
109
+ # what the package needs. A package that needs a file its manifest does
110
+ # not declare must have that file added to the manifest, not bundled some
111
+ # other way.
112
+ # A directory import first checks with the station which files it already
113
+ # has (by digest, including across every other cached revision) — a
114
+ # retry after an interrupted upload resumes instead of re-sending
115
+ # everything. Only what's actually missing is uploaded, with a progress
116
+ # bar over just those bytes; the command then reports how many files were
117
+ # reused, e.g. "12 of 24 files already on the station (302 MB reused)".
118
+ # An existing .zip/.tar.gz archive still uploads as a single one-shot
119
+ # transfer. --output json moves all of the above to stderr so stdout
120
+ # carries nothing but the final JSON — see "Machine-readable output".
121
+
122
+ lumentest sequence list [--article ARTICLE] [--output rich|json]
123
+ lumentest sequence steps ARTICLE SEQUENCE
124
+ lumentest sequence selection get ARTICLE SEQUENCE
125
+ lumentest sequence selection set ARTICLE SEQUENCE --steps 1,2,5
126
+ lumentest sequence selection clear ARTICLE SEQUENCE
127
+
128
+ lumentest test start [--serial SN] [--article ARTICLE] [--sequence NAME]
129
+ # Blocks with an interactive Rich UI; handles ALERT prompts; exits with
130
+ # the run's terminal status: 0 PASS, 1 FAIL, 2 ABORTED, 3 ERROR (outcome
131
+ # undetermined — station unreachable or run gone). The operator comes
132
+ # from the bearer token established by `lumentest login`, not a flag.
133
+ # The run header shows the package's Build Words when the station reports
134
+ # them.
135
+ lumentest test step SEQUENCE STEP [--article ARTICLE] [--serial SN]
136
+ # Runs one step via the same interactive UI. STEP is a name or 0-based index.
137
+ lumentest test status [--output rich|json]
138
+ # One-shot snapshot of the currently active test run.
139
+
140
+ lumentest result list [--limit N] [--serial SN] [--output rich|json]
141
+ # --serial SN filters to runs for one device (exact dut_id match); the station
142
+ # filters server-side so high-throughput stations don't ship the full history.
143
+ lumentest result show [RUN_ID]
144
+ # RUN_ID is optional: omit it to show the most recent run.
145
+ lumentest result logs [RUN_ID] [--step IDX] [--stream stdout|stderr|traceback]
146
+ [--download DIR] [--output rich|json]
147
+ # RUN_ID is optional: omit it to fetch the most recent run's logs.
148
+ # Without --step: fetches the complete log bundle via GET /logs/all — run error,
149
+ # structured journal, and every step's stdout/stderr/traceback. --output json
150
+ # emits the raw JSON bundle; --download DIR writes the tree to DIR/<run_id>/
151
+ # (error.txt, log.jsonl, steps/<name>/{stdout,stderr,traceback}.txt, omitting
152
+ # empty files). --output json takes precedence when both flags are given.
153
+ # With --step IDX: shows that step's stdout, stderr, and traceback (or a single
154
+ # stream via --stream).
155
+ lumentest result export [RUN_ID] [--all] [--serial SN] [--limit N]
156
+ [--out DIR] [--output rich|json]
157
+ # Downloads one .tar.gz station snapshot via GET /api/results/export: the
158
+ # selected run(s) verbatim under results/ (run.json, qrm-payload.json, the
159
+ # journal, every step's captured streams) plus the station's own log,
160
+ # state.json, operator_stats.json and effective config under station/ —
161
+ # laid out exactly as the station stores them on disk. RUN_ID / --all /
162
+ # --serial / --limit govern results/ only; the station context is always
163
+ # included. Bare `result export` packs only the most recent run; --all
164
+ # opts into every retained run. RUN_ID is mutually exclusive with --all,
165
+ # --serial and --limit.
166
+ # --out DIR: directory to write the archive into (default: the current
167
+ # directory). The filename is stamped by the station itself from its own
168
+ # [station].id and a UTC timestamp, so an export stays identifiable once
169
+ # it is off the station.
170
+ # artifact_cache/ package binaries are never packed — represented instead by
171
+ # an inventory (article, revision, size, file count); Arena is the system
172
+ # of record for firmware. Re-send a run to QRM by hand with
173
+ # `qrm result upload <run>/qrm-payload.json` against the extracted tree.
174
+ # An export contains the station's QRM service password, session token
175
+ # hashes, operator identities, and whatever a step or tool printed. Treat
176
+ # an archive as a secret — see doc/troubleshooting.md § Collecting a
177
+ # station snapshot for support.
178
+
179
+ lumentest config list [--output rich|json]
180
+ # Flattens config.toml to dotted keys (station.id, env.NRF_TOOLS,
181
+ # timeouts.default_step_s, ...) so a technician can scan the whole
182
+ # effective configuration in one table. Secrets (qrm.password,
183
+ # serial.client_secret, user password hashes) show as "<redacted>" and are
184
+ # marked in the Notes column, alongside keys that need a station restart
185
+ # to take effect (station.host, station.port, debug.results_dir).
186
+ lumentest config get KEY [--output rich|json]
187
+ lumentest config set KEY VALUE [--json] [--output rich|json]
188
+ # VALUE is sent as a raw string and coerced through the server's pydantic
189
+ # validation (e.g. "90" -> the int 90); pass --json to send VALUE parsed
190
+ # as JSON instead, for list/dict/explicitly-typed values. This is how a
191
+ # newly required package env variable gets set without restarting the
192
+ # station: `lumentest config set env.SOME_VAR value` then
193
+ # `lumentest product scan`. A key flagged restart-required still saves,
194
+ # but a visible warning says it only takes effect after the next restart.
195
+ # Setting a secret back to its own masked "<redacted>" value is rejected,
196
+ # so a `config get` / `config set` round trip can never silently
197
+ # overwrite a credential.
198
+ lumentest config unset KEY
199
+ # Resets KEY to its model default; for the free-form [env] and
200
+ # [test].fake_tools maps this removes the entry instead. A required key
201
+ # with no default (e.g. station.id) is rejected. A restart-required key
202
+ # carries the same warning `config set` gives — the reset is saved, but
203
+ # the running station keeps the old value until it restarts.
204
+
205
+ lumentest operator whoami [--output rich|json]
206
+ # Resolve the operator behind the stored token via GET /api/auth/whoami.
207
+ # Exits 1 if the token is missing, unknown, or expired.
208
+
209
+ lumentest operator stats [OPERATOR_ID] [--stations URL1,URL2,...] [--output rich|json]
210
+ # Per-operator statistics aggregated across stations (counters add,
211
+ # last_login = max). Omit OPERATOR_ID to list every operator seen.
212
+ # --stations defaults to the logged-in station.
213
+ ```
214
+
215
+ ## Machine-readable output
216
+
217
+ Every command above that takes `--output json` writes **exactly one JSON document
218
+ to stdout and nothing else**, so a caller can pipe it straight into a parser:
219
+
220
+ ```bash
221
+ lumentest product import --bundle --output json ./400-1011_A | jq -r .build_words
222
+ ```
223
+
224
+ Everything meant for a human — progress bars, warnings, "generated the
225
+ manifest" — goes to **stderr** in JSON mode, and errors always do. Redirect it
226
+ away with `2>/dev/null`, or keep it to watch a long import while still capturing
227
+ the result.
228
+
229
+ A command that finds nothing to report emits `null` rather than staying silent,
230
+ so stdout parses unconditionally.
231
+
232
+ `result logs --step IDX` is the one exception: it has no JSON form, so `--output`
233
+ is inert there and the step's captured streams go to stdout either way. Piping
234
+ that output works with the flag or without it.
235
+
236
+ In `--output rich` (the default) the human output goes to stdout as usual.
237
+
238
+ ## Server URL
239
+
240
+ The CLI reads the server URL from (in order of priority):
241
+
242
+ 1. `--base-url` flag on `lumentest login`
243
+ 2. `LUMENTEST_URL` environment variable
244
+ 3. Stored value in `~/.config/lumentest/login.json`
245
+ 4. Default: `http://127.0.0.1:17080`
@@ -0,0 +1,213 @@
1
+ # lr-lumentest-cli
2
+
3
+ CLI for the LumenRadio LumenTest production test station server.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install lr-lumentest-cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Point the CLI at your station server and identify yourself by scanning
15
+ # your badge (the handheld scanner types the badge id at the prompt)
16
+ lumentest login --base-url http://10.0.0.1:17080 --badge-id LOP0000001
17
+
18
+ # Check server health and login state
19
+ lumentest status
20
+
21
+ # Scan and list imported products
22
+ lumentest product scan
23
+ lumentest product list
24
+
25
+ # Run a full test sequence interactively (prompts on ALERT steps)
26
+ lumentest test start --serial SN-00001
27
+
28
+ # Run a single step (for development / debugging)
29
+ lumentest test step production-test flash-firmware --serial SN-00001
30
+ ```
31
+
32
+ ## Command Reference
33
+
34
+ ```
35
+ lumentest login [--base-url URL] --badge-id ID
36
+ # ID is the scanned QR badge; the server resolves it to your operator id.
37
+ lumentest logout
38
+ lumentest status [--output rich|json]
39
+ lumentest --version
40
+
41
+ lumentest product list [--output rich|json]
42
+ # Table includes a Build Words column — see doc/build-words.md. The manifest
43
+ # digest is not shown: read it from --output json when you need the digest.
44
+ lumentest product active
45
+ lumentest product set ARTICLE [REVISION]
46
+ lumentest product remove ARTICLE [REVISION] [--yes]
47
+ lumentest product scan [--rescan] [--output rich|json]
48
+ # Scans the station's artifact_cache/, registers every valid package, and
49
+ # reports each package's declared env variables (from parameters.toml) —
50
+ # the value a run will see, where it came from (config.toml's [env], the
51
+ # station's own environment, or the package default) and which are still
52
+ # unconfigured — alongside its prerequisites (the external tools its
53
+ # sequences need) against the station itself. Each unconfigured variable's
54
+ # line carries the exact `lumentest config set env.NAME <VALUE>` command to
55
+ # fix it; a set value applies to the running station immediately, so
56
+ # re-running scan is enough to confirm it, for a variable and for a tool
57
+ # newly installed on PATH alike — no restart needed either way. --output
58
+ # json emits the raw payload.
59
+ lumentest product import PATH [--update] [--bundle] [--output rich|json]
60
+ # PATH: directory or .zip/.tar.gz archive.
61
+ # --update: recompute SHA-256 checksums in artifact-manifest.json before
62
+ # uploading — useful during step development to skip manual hashing.
63
+ # --bundle: pack PATH into <PATH>.tar.gz (manifest + declared files only)
64
+ # next to it instead of uploading — for email/USB delivery;
65
+ # import the archive later with the same command. Requires a
66
+ # directory; passing an archive file just warns and exits.
67
+ # Prints the package's Build Words (doc/build-words.md) so a
68
+ # technician can read them off a locally built bundle without a
69
+ # running server; --output json carries them as "build_words".
70
+ # Reports the package's declared env variables the same way `product scan`
71
+ # does: the value a run will see, where it came from, and which still need
72
+ # a value. Fix an unconfigured one with the `lumentest config set` command
73
+ # printed on its line, then re-run `product scan` to confirm — same as
74
+ # confirming a newly installed prerequisite tool, neither needs a restart.
75
+ # Only artifact-manifest.json's declared files (and declared extra
76
+ # sequences) are packed and uploaded — the manifest is the contract for
77
+ # what the package needs. A package that needs a file its manifest does
78
+ # not declare must have that file added to the manifest, not bundled some
79
+ # other way.
80
+ # A directory import first checks with the station which files it already
81
+ # has (by digest, including across every other cached revision) — a
82
+ # retry after an interrupted upload resumes instead of re-sending
83
+ # everything. Only what's actually missing is uploaded, with a progress
84
+ # bar over just those bytes; the command then reports how many files were
85
+ # reused, e.g. "12 of 24 files already on the station (302 MB reused)".
86
+ # An existing .zip/.tar.gz archive still uploads as a single one-shot
87
+ # transfer. --output json moves all of the above to stderr so stdout
88
+ # carries nothing but the final JSON — see "Machine-readable output".
89
+
90
+ lumentest sequence list [--article ARTICLE] [--output rich|json]
91
+ lumentest sequence steps ARTICLE SEQUENCE
92
+ lumentest sequence selection get ARTICLE SEQUENCE
93
+ lumentest sequence selection set ARTICLE SEQUENCE --steps 1,2,5
94
+ lumentest sequence selection clear ARTICLE SEQUENCE
95
+
96
+ lumentest test start [--serial SN] [--article ARTICLE] [--sequence NAME]
97
+ # Blocks with an interactive Rich UI; handles ALERT prompts; exits with
98
+ # the run's terminal status: 0 PASS, 1 FAIL, 2 ABORTED, 3 ERROR (outcome
99
+ # undetermined — station unreachable or run gone). The operator comes
100
+ # from the bearer token established by `lumentest login`, not a flag.
101
+ # The run header shows the package's Build Words when the station reports
102
+ # them.
103
+ lumentest test step SEQUENCE STEP [--article ARTICLE] [--serial SN]
104
+ # Runs one step via the same interactive UI. STEP is a name or 0-based index.
105
+ lumentest test status [--output rich|json]
106
+ # One-shot snapshot of the currently active test run.
107
+
108
+ lumentest result list [--limit N] [--serial SN] [--output rich|json]
109
+ # --serial SN filters to runs for one device (exact dut_id match); the station
110
+ # filters server-side so high-throughput stations don't ship the full history.
111
+ lumentest result show [RUN_ID]
112
+ # RUN_ID is optional: omit it to show the most recent run.
113
+ lumentest result logs [RUN_ID] [--step IDX] [--stream stdout|stderr|traceback]
114
+ [--download DIR] [--output rich|json]
115
+ # RUN_ID is optional: omit it to fetch the most recent run's logs.
116
+ # Without --step: fetches the complete log bundle via GET /logs/all — run error,
117
+ # structured journal, and every step's stdout/stderr/traceback. --output json
118
+ # emits the raw JSON bundle; --download DIR writes the tree to DIR/<run_id>/
119
+ # (error.txt, log.jsonl, steps/<name>/{stdout,stderr,traceback}.txt, omitting
120
+ # empty files). --output json takes precedence when both flags are given.
121
+ # With --step IDX: shows that step's stdout, stderr, and traceback (or a single
122
+ # stream via --stream).
123
+ lumentest result export [RUN_ID] [--all] [--serial SN] [--limit N]
124
+ [--out DIR] [--output rich|json]
125
+ # Downloads one .tar.gz station snapshot via GET /api/results/export: the
126
+ # selected run(s) verbatim under results/ (run.json, qrm-payload.json, the
127
+ # journal, every step's captured streams) plus the station's own log,
128
+ # state.json, operator_stats.json and effective config under station/ —
129
+ # laid out exactly as the station stores them on disk. RUN_ID / --all /
130
+ # --serial / --limit govern results/ only; the station context is always
131
+ # included. Bare `result export` packs only the most recent run; --all
132
+ # opts into every retained run. RUN_ID is mutually exclusive with --all,
133
+ # --serial and --limit.
134
+ # --out DIR: directory to write the archive into (default: the current
135
+ # directory). The filename is stamped by the station itself from its own
136
+ # [station].id and a UTC timestamp, so an export stays identifiable once
137
+ # it is off the station.
138
+ # artifact_cache/ package binaries are never packed — represented instead by
139
+ # an inventory (article, revision, size, file count); Arena is the system
140
+ # of record for firmware. Re-send a run to QRM by hand with
141
+ # `qrm result upload <run>/qrm-payload.json` against the extracted tree.
142
+ # An export contains the station's QRM service password, session token
143
+ # hashes, operator identities, and whatever a step or tool printed. Treat
144
+ # an archive as a secret — see doc/troubleshooting.md § Collecting a
145
+ # station snapshot for support.
146
+
147
+ lumentest config list [--output rich|json]
148
+ # Flattens config.toml to dotted keys (station.id, env.NRF_TOOLS,
149
+ # timeouts.default_step_s, ...) so a technician can scan the whole
150
+ # effective configuration in one table. Secrets (qrm.password,
151
+ # serial.client_secret, user password hashes) show as "<redacted>" and are
152
+ # marked in the Notes column, alongside keys that need a station restart
153
+ # to take effect (station.host, station.port, debug.results_dir).
154
+ lumentest config get KEY [--output rich|json]
155
+ lumentest config set KEY VALUE [--json] [--output rich|json]
156
+ # VALUE is sent as a raw string and coerced through the server's pydantic
157
+ # validation (e.g. "90" -> the int 90); pass --json to send VALUE parsed
158
+ # as JSON instead, for list/dict/explicitly-typed values. This is how a
159
+ # newly required package env variable gets set without restarting the
160
+ # station: `lumentest config set env.SOME_VAR value` then
161
+ # `lumentest product scan`. A key flagged restart-required still saves,
162
+ # but a visible warning says it only takes effect after the next restart.
163
+ # Setting a secret back to its own masked "<redacted>" value is rejected,
164
+ # so a `config get` / `config set` round trip can never silently
165
+ # overwrite a credential.
166
+ lumentest config unset KEY
167
+ # Resets KEY to its model default; for the free-form [env] and
168
+ # [test].fake_tools maps this removes the entry instead. A required key
169
+ # with no default (e.g. station.id) is rejected. A restart-required key
170
+ # carries the same warning `config set` gives — the reset is saved, but
171
+ # the running station keeps the old value until it restarts.
172
+
173
+ lumentest operator whoami [--output rich|json]
174
+ # Resolve the operator behind the stored token via GET /api/auth/whoami.
175
+ # Exits 1 if the token is missing, unknown, or expired.
176
+
177
+ lumentest operator stats [OPERATOR_ID] [--stations URL1,URL2,...] [--output rich|json]
178
+ # Per-operator statistics aggregated across stations (counters add,
179
+ # last_login = max). Omit OPERATOR_ID to list every operator seen.
180
+ # --stations defaults to the logged-in station.
181
+ ```
182
+
183
+ ## Machine-readable output
184
+
185
+ Every command above that takes `--output json` writes **exactly one JSON document
186
+ to stdout and nothing else**, so a caller can pipe it straight into a parser:
187
+
188
+ ```bash
189
+ lumentest product import --bundle --output json ./400-1011_A | jq -r .build_words
190
+ ```
191
+
192
+ Everything meant for a human — progress bars, warnings, "generated the
193
+ manifest" — goes to **stderr** in JSON mode, and errors always do. Redirect it
194
+ away with `2>/dev/null`, or keep it to watch a long import while still capturing
195
+ the result.
196
+
197
+ A command that finds nothing to report emits `null` rather than staying silent,
198
+ so stdout parses unconditionally.
199
+
200
+ `result logs --step IDX` is the one exception: it has no JSON form, so `--output`
201
+ is inert there and the step's captured streams go to stdout either way. Piping
202
+ that output works with the flag or without it.
203
+
204
+ In `--output rich` (the default) the human output goes to stdout as usual.
205
+
206
+ ## Server URL
207
+
208
+ The CLI reads the server URL from (in order of priority):
209
+
210
+ 1. `--base-url` flag on `lumentest login`
211
+ 2. `LUMENTEST_URL` environment variable
212
+ 3. Stored value in `~/.config/lumentest/login.json`
213
+ 4. Default: `http://127.0.0.1:17080`
@@ -0,0 +1,48 @@
1
+ # -*- mode: python ; coding: utf-8 -*-
2
+ from PyInstaller.utils.hooks import (
3
+ copy_metadata,
4
+ collect_data_files,
5
+ collect_submodules,
6
+ )
7
+
8
+ a = Analysis(
9
+ ["src/lumentest/__main__.py"],
10
+ pathex=[],
11
+ binaries=[],
12
+ datas=[
13
+ *copy_metadata("lr-lumentest-cli"),
14
+ *collect_data_files("rich"),
15
+ ],
16
+ # rich.cells lazily import_module()s a Unicode-version-specific submodule
17
+ # (e.g. rich._unicode_data.unicode17-0-0) whose name is computed at runtime,
18
+ # so PyInstaller's static analysis can't discover it. Collect them all.
19
+ hiddenimports=[*collect_submodules("rich._unicode_data")],
20
+ hookspath=[],
21
+ hooksconfig={},
22
+ runtime_hooks=[],
23
+ excludes=[],
24
+ noarchive=False,
25
+ )
26
+
27
+ pyz = PYZ(a.pure)
28
+
29
+ exe = EXE(
30
+ pyz,
31
+ a.scripts,
32
+ a.binaries,
33
+ a.datas,
34
+ [],
35
+ name="lumentest",
36
+ debug=False,
37
+ bootloader_ignore_signals=False,
38
+ strip=False,
39
+ upx=True,
40
+ upx_exclude=[],
41
+ runtime_tmpdir=None,
42
+ console=True,
43
+ disable_windowed_traceback=False,
44
+ argv_emulation=False,
45
+ target_arch=None,
46
+ codesign_identity=None,
47
+ entitlements_file=None,
48
+ )